@yolk-sdk/codemode 0.1.0-canary.106 → 0.1.0-canary.108

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing } from "./catalog.mjs";
1
+ import { CodeModeCatalogSummary, codeModeCatalog, codeModeCatalogInstruction, codeModeCatalogInstructionKey, codeModeStageOnlyCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeCatalog, renderCodeModeDescription, renderCodeModeInstructedDescription, selectCodeModeListing, summarizeCodeModeCatalog } from "./catalog.mjs";
2
2
  import { ClassifierToolParams, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool } from "./classifier-tool.mjs";
3
3
  import { codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools } from "./search.mjs";
4
4
  import { boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls } from "./output.mjs";
5
5
  import { codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool } from "./tool.mjs";
6
6
  import { codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars } from "./store.mjs";
7
- export { ClassifierToolParams, boundCodeModeSegments, classifierToolName, codeModeCatalog, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeResultSegments, codeModeSearchTokens, codeModeSegmentsContent, codeModeStoreFromToolResults, codeModeToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, defaultCodeModeInlineBudget, defaultCodeModeLimits, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, defaultCodeModeSearchLimit, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, makeClassifierConcurrencyLimiter, makeClassifierTool, makeCodeModeTool, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars, renderCodeModeDescription, searchCodeModeTools, selectCodeModeListing, summarizeCodeModeCalls };
7
+ export { ClassifierToolParams, CodeModeCatalogSummary, boundCodeModeSegments, classifierToolName, codeModeCatalog, codeModeCatalogInstruction, codeModeCatalogInstructionKey, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeResultSegments, codeModeSearchTokens, codeModeSegmentsContent, codeModeStageOnlyCatalog, codeModeStoreFromToolResults, codeModeToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, defaultCodeModeInlineBudget, defaultCodeModeLimits, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, defaultCodeModeSearchLimit, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, makeClassifierConcurrencyLimiter, makeClassifierTool, makeCodeModeTool, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars, renderCodeModeCatalog, renderCodeModeDescription, renderCodeModeInstructedDescription, searchCodeModeTools, selectCodeModeListing, summarizeCodeModeCalls, summarizeCodeModeCatalog };
@@ -1 +1 @@
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"}
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;AAwblC;;;cAAa,sBAAA,GACX,OAAA,GAAS,yBAAA,KACR,gBAgFF"}
package/dist/node.mjs CHANGED
@@ -61,7 +61,7 @@ const wrapperSuffix = "\n}";
61
61
  * proxy (close-match suggestions). Not a security boundary (`globalThis.tools` is unguarded):
62
62
  * arguments are still decoded by the host.
63
63
  */
64
- const toolsGuardSource = `(function (tools, labels) {
64
+ const toolsGuardSource = `(function (tools, labels, unbound) {
65
65
  // Captured before the script runs, so a script that changes built-ins cannot change the checks.
66
66
  const { apply, getPrototypeOf, setPrototypeOf } = Reflect
67
67
  const isFinite = Number.isFinite
@@ -199,16 +199,23 @@ const toolsGuardSource = `(function (tools, labels) {
199
199
  if (!wrappers.has(call)) wrappers.set(call, wrap('tools[' + JSON.stringify(key) + ']', call))
200
200
  guarded[key] = wrappers.get(call)
201
201
  }
202
+ // Unbound keys (stage-only tools) reject with their hint instead of being undefined.
203
+ const rejections = create(null)
204
+ for (const [key, message] of unbound) {
205
+ if (!(key in guarded)) rejections[key] = async () => { throw new TypeErrorCtor(message) }
206
+ }
202
207
  return new Proxy(Object.freeze(guarded), {
203
- get: (target, key) => (key in target ? target[key] : tools[key])
208
+ get: (target, key) => (key in target ? target[key] : key in rejections ? rejections[key] : tools[key])
204
209
  })
205
210
  })`;
206
211
  const guardPrefix = "return (async (tools, console) => {";
207
212
  /** JSON as a JavaScript expression (U+2028/U+2029 escaped for older parsers). */
208
213
  const jsonSource = (value) => JSON.stringify(value).replace(/[\u2028\u2029]/g, (char) => char === "\u2028" ? "\\u2028" : "\\u2029");
209
- const guardSuffix = (tools) => {
214
+ const guardSuffix = (tools, unbound) => {
210
215
  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)`;
216
+ const pairs = tools.map((tool, index) => [tool.name, tool.callLabel ?? labels[index] ?? `tools[${JSON.stringify(tool.name)}]`]);
217
+ const rejected = (unbound ?? []).map((entry) => [entry.key, entry.message]);
218
+ return `\n})(${toolsGuardSource}(tools, ${jsonSource(pairs)}, ${jsonSource(rejected)}), console)`;
212
219
  };
213
220
  const lineTerminators = /\r\n?|[\n\u2028\u2029]/g;
214
221
  /** Lines of the script as QuickJS counts them (at least; never fewer). */
@@ -360,7 +367,7 @@ const makePiCodeModeExecutor = (options = {}) => {
360
367
  });
361
368
  }
362
369
  try {
363
- return fromPiResult(await sandbox.execute(`${guardPrefix}${stripped.code}${guardSuffix(execution.tools)}`, {
370
+ return fromPiResult(await sandbox.execute(`${guardPrefix}${stripped.code}${guardSuffix(execution.tools, execution.unbound)}`, {
364
371
  signal: execution.signal,
365
372
  store: execution.store,
366
373
  timeoutMs
package/dist/node.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.mjs","names":[],"sources":["../src/node.ts"],"sourcesContent":["import * as nodeModule from 'node:module'\nimport { Option, Predicate } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n CodemodeSandbox,\n type CodemodeResult,\n type CodemodeSandboxOptions,\n type CodemodeTool,\n type CodemodeWasmModule\n} from '@earendil-works/pi-codemode'\nimport { codeModeCallLabels } from './catalog.ts'\nimport type {\n CodeModeError,\n CodeModeExecuteOptions,\n CodeModeExecutionResult,\n CodeModeExecutor,\n CodeModeExecutorGlobal,\n CodeModeExecutorTool,\n CodeModeStoreWrites\n} from './executor.ts'\n\n/** Default cap of concurrent script executions per executor. */\nexport const defaultMaxConcurrentExecutions = 4\n\nexport type PiCodeModeExecutorOptions = {\n /**\n * Compiled `quickjs-wasi/quickjs.wasm` (see pi's `loadQuickJSWasm`). Default: the file of the\n * installed `quickjs-wasi` package, compiled once per process.\n */\n readonly wasm?: CodemodeWasmModule | Promise<CodemodeWasmModule>\n /**\n * Worker entry that imports `@earendil-works/pi-codemode/worker`. Default: pi's own worker file,\n * which exists on disk when the package is not bundled (Next `serverExternalPackages`).\n */\n readonly workerUrl?: string | URL\n /** Concurrent executions of this executor; more wait for a free slot. Default 4. */\n readonly maxConcurrentExecutions?: number\n}\n\ntype SlotOutcome = 'acquired' | 'aborted' | 'timeout'\n\ntype Waiter = (outcome: SlotOutcome) => void\n\n/** Abort- and deadline-aware counting semaphore for executions. */\nconst makeExecutionSlots = (max: number) => {\n let active = 0\n const waiting: Array<Waiter> = []\n\n const acquire = (signal: AbortSignal, timeoutMs: number): Promise<SlotOutcome> => {\n if (signal.aborted) return Promise.resolve('aborted')\n\n if (active < max) {\n active++\n\n return Promise.resolve('acquired')\n }\n\n return new Promise(resolve => {\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const settle: Waiter = outcome => {\n const index = waiting.indexOf(settle)\n\n if (index !== -1) waiting.splice(index, 1)\n\n if (timer !== undefined) clearTimeout(timer)\n signal.removeEventListener('abort', onAbort)\n resolve(outcome)\n }\n\n const onAbort = () => settle('aborted')\n\n waiting.push(settle)\n signal.addEventListener('abort', onAbort, { once: true })\n\n if (Number.isFinite(timeoutMs)) {\n timer = setTimeout(() => settle('timeout'), Math.max(0, timeoutMs))\n }\n })\n }\n\n const release = () => {\n const next = waiting[0]\n\n if (next === undefined) {\n active--\n } else {\n // The slot passes straight to the next waiter.\n next('acquired')\n }\n }\n\n return { acquire, release }\n}\n\nconst wrapperPrefix = 'async function __yolkCodeMode() {\\n'\n\nconst wrapperSuffix = '\\n}'\n\n/** Most values one argument may hold; a larger argument is rejected. */\nconst maxArgumentValues = 100_000\n\n/** Deepest object/array nesting one argument may have; deeper arguments are rejected. */\nconst maxArgumentDepth = 64\n\n/**\n * Evaluated in the VM before the script runs: `(tools, labels) => guardedTools`, with `labels` the\n * `[name, callLabel]` pairs of the tools in order (`codeModeCallLabels`). Each call walks its first\n * argument once, the way `JSON.stringify` reads it (each own enumerable string key and each array\n * index read once, so getters run once; a callable `toJSON` is called once with the key, `''` at\n * the root, and its result is walked at the same path), and builds a fresh copy (null-prototype\n * objects and arrays) that is what the call sends. It rejects with a `TypeError` naming the call\n * label and path when JSON would change the value instead of carrying it: non-finite numbers,\n * invalid `Date`s, `undefined`/function/symbol array items and holes, function/symbol/bigint\n * values, cycles, and objects that are neither plain (prototype `Object.prototype` or `null`) nor\n * arrays and have no `toJSON`. An argument with more than `maxArgumentValues` values or nested more\n * than `maxArgumentDepth` levels is rejected too, never sent unchecked. `undefined`-valued keys are\n * left out (absent, as JSON drops them); the same object twice (not a cycle) is copied twice. An\n * error thrown by a getter or `toJSON` rejects the call with that error. A rejected call never\n * reaches the host. Every member of pi's `tools` is wrapped; unknown members fall through to pi's\n * proxy (close-match suggestions). Not a security boundary (`globalThis.tools` is unguarded):\n * arguments are still decoded by the host.\n */\nconst toolsGuardSource = `(function (tools, labels) {\n // Captured before the script runs, so a script that changes built-ins cannot change the checks.\n const { apply, getPrototypeOf, setPrototypeOf } = Reflect\n const isFinite = Number.isFinite\n const isArray = Array.isArray\n const { keys, create, defineProperty } = Object\n const getTime = Date.prototype.getTime\n const TypeErrorCtor = TypeError\n const SetCtor = Set\n const setAdd = Set.prototype.add\n const setHas = Set.prototype.has\n const setDelete = Set.prototype.delete\n const ObjectProto = Object.prototype\n const { trunc, min } = Math\n const stringify = JSON.stringify\n const plainJson = 'pass plain JSON (objects, arrays, strings, finite numbers, booleans, null)'\n const maxValues = ${maxArgumentValues}\n const maxDepth = ${maxArgumentDepth}\n class Rejected {\n constructor(message) {\n defineProperty(this, 'message', { value: message })\n }\n }\n const keyPath = (path, key) =>\n /^[A-Za-z_$][\\\\w$]*$/.test(key)\n ? (path === '' ? key : path + '.' + key)\n : path + '[' + stringify(key) + ']'\n const typeName = value => {\n const proto = getPrototypeOf(value)\n const name = proto && typeof proto.constructor === 'function' ? proto.constructor.name : ''\n return name ? (/^[AEIOU]/.test(name) ? 'an ' : 'a ') + name : 'a non-plain object'\n }\n // The JSON copy of a value already read from its holder at key (undefined: absent), or throws\n // Rejected with why JSON would not carry it unchanged.\n const snapshot = (read, key, path, slot, walk) => {\n const at = path === '' ? 'is ' : 'at ' + path + ' is '\n if (++walk.values > maxValues) {\n throw new Rejected('has more than ' + maxValues + ' values; split the work across calls')\n }\n let value = read\n const kind = typeof value\n if ((kind === 'object' && value !== null) || kind === 'function' || kind === 'bigint') {\n // The intrinsic getTime reads a Date's internal time (and throws for anything else).\n let time\n try {\n time = apply(getTime, value, [])\n } catch {\n time = undefined\n }\n if (time !== undefined && !isFinite(time)) {\n throw new Rejected(at + 'an invalid Date; pass a valid Date or an ISO string')\n }\n const toJSON = value.toJSON\n if (typeof toJSON === 'function') value = apply(toJSON, value, [key])\n }\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return value\n case 'number':\n if (isFinite(value)) return value\n throw new Rejected(\n at + value + '; pass a finite number' + (slot === 'key' ? ' or omit the key' : '')\n )\n case 'undefined':\n if (slot === 'item') throw new Rejected(at + 'undefined; arrays cannot hold undefined')\n return undefined\n case 'bigint':\n case 'function':\n case 'symbol':\n throw new Rejected(at + 'a ' + typeof value + '; ' + plainJson)\n }\n if (value === null) return null\n if (apply(setHas, walk.ancestors, [value])) {\n throw new Rejected(at + 'a circular reference; ' + plainJson)\n }\n if (walk.depth >= maxDepth) {\n throw new Rejected('at ' + path + ' is nested more than ' + maxDepth + ' levels deep')\n }\n const array = isArray(value)\n if (!array) {\n const proto = getPrototypeOf(value)\n if (proto !== ObjectProto && proto !== null) {\n throw new Rejected(at + typeName(value) + '; ' + plainJson)\n }\n }\n apply(setAdd, walk.ancestors, [value])\n walk.depth++\n let copy\n if (array) {\n // LengthOfArrayLike, read once, as JSON.stringify does.\n // Unary plus is ToNumber: one coercion, and it throws for a bigint as JSON.stringify does.\n const raw = +value.length\n const length = raw > 0 ? min(trunc(raw), 9007199254740991) : 0\n // No prototype: no inherited toJSON or index setter can change the checked copy when pi\n // serializes it (still an array to JSON.stringify).\n copy = []\n setPrototypeOf(copy, null)\n for (let i = 0; i < length; i++) {\n copy[i] = snapshot(value[i], '' + i, path + '[' + i + ']', 'item', walk)\n }\n } else {\n copy = create(null)\n // Indexed over the fresh key array: no iterator a script could replace.\n const names = keys(value)\n for (let i = 0; i < names.length; i++) {\n const name = names[i]\n const item = snapshot(value[name], name, keyPath(path, name), 'key', walk)\n if (item !== undefined) copy[name] = item\n }\n }\n walk.depth--\n apply(setDelete, walk.ancestors, [value])\n return copy\n }\n const wrap = (label, call) => async (...args) => {\n let json\n try {\n json = snapshot(args[0], '', '', 'root', { values: 0, depth: 0, ancestors: new SetCtor() })\n } catch (error) {\n if (error instanceof Rejected) throw new TypeErrorCtor(label + ': argument ' + error.message)\n throw error\n }\n return call(json)\n }\n // Each tool's function sits at tools[name] unless an earlier tool holds that key, in which case\n // that earlier function is already wrapped.\n const wrappers = new Map()\n for (const [name, label] of labels) {\n const call = name in tools ? tools[name] : undefined\n if (typeof call === 'function' && !wrappers.has(call)) wrappers.set(call, wrap(label, call))\n }\n const guarded = Object.create(null)\n for (const key of Object.keys(tools)) {\n const call = tools[key]\n if (!wrappers.has(call)) wrappers.set(call, wrap('tools[' + JSON.stringify(key) + ']', call))\n guarded[key] = wrappers.get(call)\n }\n return new Proxy(Object.freeze(guarded), {\n get: (target, key) => (key in target ? target[key] : tools[key])\n })\n})`\n\n// pi runs `(async (tools, console) => {<code>\\n})`. The script runs in an inner function of the\n// same shape whose `tools` is guarded; the prefix shares line 1 with the script (only line-1\n// columns shift) and the guard follows the script, so its line numbers are unchanged.\nconst guardPrefix = 'return (async (tools, console) => {'\n\n/** JSON as a JavaScript expression (U+2028/U+2029 escaped for older parsers). */\nconst jsonSource = (value: unknown) =>\n JSON.stringify(value).replace(/[\\u2028\\u2029]/g, char =>\n char === '\\u2028' ? '\\\\u2028' : '\\\\u2029'\n )\n\nconst guardSuffix = (tools: ReadonlyArray<CodeModeExecutorTool>) => {\n const names = tools.map(tool => tool.name)\n const labels = codeModeCallLabels(names)\n\n // `makeCodeModeTool` supplies each label; other callers get pi's binding rule.\n const pairs = tools.map((tool, index) => [\n tool.name,\n tool.callLabel ?? labels[index] ?? `tools[${JSON.stringify(tool.name)}]`\n ])\n\n return `\\n})(${toolsGuardSource}(tools, ${jsonSource(pairs)}), console)`\n}\n\nconst lineTerminators = /\\r\\n?|[\\n\\u2028\\u2029]/g\n\n/** Lines of the script as QuickJS counts them (at least; never fewer). */\nconst scriptLineCount = (code: string) => (code.match(lineTerminators)?.length ?? 0) + 1\n\nconst scriptFrame = /codemode\\.js:(\\d+):\\d+/\n\n/** Drops stack frames of the wrapper and guard, which sit below the script's last line. */\nconst withoutWrapperFrames = (stack: string, scriptLines: number) =>\n stack\n .split('\\n')\n .filter(line => {\n const frame = scriptFrame.exec(line)\n\n return frame === null || Number(frame[1]) <= scriptLines\n })\n .join('\\n')\n\nconst ErrorWithCode = Schema.Struct({ code: Schema.String })\n\nconst errorCode = (error: unknown) =>\n Option.getOrUndefined(Option.map(Schema.decodeUnknownOption(ErrorWithCode)(error), e => e.code))\n\nconst syntaxLocation = (error: Error) => {\n const match = /^[^\\n]*:(\\d+)\\n([^\\n]*)\\n([^\\n]*)\\n/.exec(error.stack ?? '')\n\n if (match === null) return ''\n\n const line = Number(match[1]) - 1\n\n return line >= 1 ? ` (line ${line})\\n${match[2] ?? ''}\\n${match[3] ?? ''}` : ''\n}\n\nconst typeStripMessage = (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n\n if (errorCode(error) === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {\n return `${message}. Write plain JavaScript: type annotations are stripped, but enums, namespaces, and parameter properties are not supported.`\n }\n\n const location = error instanceof Error ? syntaxLocation(error) : ''\n\n return `The script could not be parsed: ${message}${location}`\n}\n\ntype Stripped =\n | { readonly ok: true; readonly code: string }\n | { readonly ok: false; readonly error: CodeModeError }\n\n/**\n * Strips TypeScript annotations with Node's `stripTypeScriptTypes` (Node 22.13+; whitespace\n * replacement keeps line and column numbers). The script is wrapped in an async function so\n * top-level `return` and `await` parse. On Node without the API, code passes through unchanged.\n */\nconst stripTypes = (code: string): Stripped => {\n if (!Predicate.isFunction(nodeModule.stripTypeScriptTypes)) return { ok: true, code }\n\n try {\n const stripped = nodeModule.stripTypeScriptTypes(`${wrapperPrefix}${code}${wrapperSuffix}`, {\n mode: 'strip'\n })\n\n if (!stripped.startsWith(wrapperPrefix) || !stripped.endsWith(wrapperSuffix)) {\n return { ok: false, error: { kind: 'script', message: 'The script could not be parsed.' } }\n }\n\n return {\n ok: true,\n code: stripped.slice(wrapperPrefix.length, stripped.length - wrapperSuffix.length)\n }\n } catch (error) {\n return { ok: false, error: { kind: 'script', message: typeStripMessage(error) } }\n }\n}\n\nconst piTool = (tool: CodeModeExecutorTool): CodemodeTool => {\n const converted: CodemodeTool = {\n name: tool.name,\n execute: (args, context) => tool.execute(args, context)\n }\n\n if (tool.description !== undefined) converted.description = tool.description\n\n if (tool.inputSchema !== undefined) converted.inputSchema = tool.inputSchema\n\n if (tool.outputSchema !== undefined) converted.outputSchema = tool.outputSchema\n\n return converted\n}\n\nconst piGlobal = (global: CodeModeExecutorGlobal): CodemodeTool => {\n const converted = piTool(global)\n\n if (global.spread !== undefined) converted.spread = global.spread\n\n if (global.signature !== undefined) converted.signature = global.signature\n\n return converted\n}\n\nconst failure = (error: CodeModeError): CodeModeExecutionResult => ({\n ok: false,\n error,\n output: []\n})\n\nconst StoreWrites = Schema.Struct({\n set: Schema.Record(Schema.String, Schema.Json),\n delete: Schema.Array(Schema.String)\n})\n\nconst decodeStoreWrites = Schema.decodeUnknownOption(StoreWrites)\n\ntype MutableCodeModeError = {\n kind: CodeModeError['kind']\n message: string\n stack?: string\n}\n\ntype MutableExecutionResult = {\n ok: boolean\n value?: unknown\n error?: CodeModeError\n output: CodeModeExecutionResult['output']\n storeWrites?: CodeModeStoreWrites\n}\n\nconst fromPiResult = (result: CodemodeResult, scriptLines: number): CodeModeExecutionResult => {\n if (result.ok) {\n const storeWrites = decodeStoreWrites(result.storeWrites)\n\n if (Option.isNone(storeWrites)) {\n return failure({\n kind: 'sandbox',\n message: 'The script reported store writes that are not JSON.'\n })\n }\n\n const converted: MutableExecutionResult = {\n ok: true,\n output: result.output,\n storeWrites: storeWrites.value\n }\n\n if (result.value !== undefined) converted.value = result.value\n\n return converted\n }\n\n const { kind, name, message, stack } = result.error\n const named = name !== undefined && name.length > 0 && !message.startsWith(name)\n\n const error: MutableCodeModeError = {\n kind,\n message: named ? `${name}: ${message}` : message\n }\n\n if (stack !== undefined) error.stack = withoutWrapperFrames(stack, scriptLines)\n\n return { ok: false, error, output: result.output }\n}\n\nconst describeError = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\n/**\n * A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM\n * (WebAssembly) in a fresh worker thread, closed when the execution ends. Applies the timeout,\n * heap cap, abort signal, and store, strips TypeScript annotations first, rejects tool arguments\n * that JSON would silently change inside the script (see `CodeModeExecutorTool`), and caps concurrent\n * executions per executor; create one executor per process (module scope) so the cap is\n * per process.\n *\n * Node only. The worker inherits a copy of `process.env` (scripts cannot read it: the VM has no\n * `process`). Under Next.js, list `@yolk-sdk/codemode`, `@earendil-works/pi-codemode`, and\n * `quickjs-wasi` in `serverExternalPackages` so the worker file and wasm stay on disk.\n */\nexport const makePiCodeModeExecutor = (\n options: PiCodeModeExecutorOptions = {}\n): CodeModeExecutor => {\n const slots = makeExecutionSlots(\n Math.max(1, Math.floor(options.maxConcurrentExecutions ?? defaultMaxConcurrentExecutions))\n )\n\n const run = async (code: string, execution: CodeModeExecuteOptions, queuedAt: number) => {\n const remaining = () => execution.timeoutMs - (Date.now() - queuedAt)\n const stripped = stripTypes(code)\n\n if (!stripped.ok) return failure(stripped.error)\n\n const timeoutMs = remaining()\n\n if (timeoutMs <= 0) {\n return failure({\n kind: 'timeout',\n message: `Execution timed out after ${execution.timeoutMs} ms`\n })\n }\n\n const sandboxOptions: CodemodeSandboxOptions = {\n tools: execution.tools.map(piTool),\n globals: execution.globals.map(piGlobal),\n timeoutMs,\n memoryLimitBytes: execution.memoryLimitBytes\n }\n\n if (options.wasm !== undefined) sandboxOptions.wasm = options.wasm\n\n if (options.workerUrl !== undefined) sandboxOptions.workerUrl = options.workerUrl\n\n let sandbox: CodemodeSandbox\n\n try {\n sandbox = new CodemodeSandbox(sandboxOptions)\n } catch (error) {\n return failure({\n kind: 'sandbox',\n message: `Could not create the sandbox: ${describeError(error)}`\n })\n }\n\n try {\n return fromPiResult(\n await sandbox.execute(`${guardPrefix}${stripped.code}${guardSuffix(execution.tools)}`, {\n signal: execution.signal,\n store: execution.store,\n timeoutMs\n }),\n scriptLineCount(stripped.code)\n )\n } finally {\n await sandbox.close()\n }\n }\n\n return {\n execute: async (code, execution) => {\n const queuedAt = Date.now()\n const slot = await slots.acquire(execution.signal, execution.timeoutMs)\n\n if (slot === 'aborted') return failure({ kind: 'aborted', message: 'Execution aborted' })\n\n if (slot === 'timeout') {\n return failure({\n kind: 'timeout',\n message: `Execution timed out after ${execution.timeoutMs} ms while waiting for a free execution slot`\n })\n }\n\n try {\n return await run(code, execution, queuedAt)\n } finally {\n slots.release()\n }\n }\n }\n}\n"],"mappings":";;;;;;;AAsBA,MAAa,iCAAiC;;AAsB9C,MAAM,sBAAsB,QAAgB;CAC1C,IAAI,SAAS;CACb,MAAM,UAAyB,CAAC;CAEhC,MAAM,WAAW,QAAqB,cAA4C;EAChF,IAAI,OAAO,SAAS,OAAO,QAAQ,QAAQ,SAAS;EAEpD,IAAI,SAAS,KAAK;GAChB;GAEA,OAAO,QAAQ,QAAQ,UAAU;EACnC;EAEA,OAAO,IAAI,SAAQ,YAAW;GAC5B,IAAI;GAEJ,MAAM,UAAiB,YAAW;IAChC,MAAM,QAAQ,QAAQ,QAAQ,MAAM;IAEpC,IAAI,UAAU,IAAI,QAAQ,OAAO,OAAO,CAAC;IAEzC,IAAI,UAAU,KAAA,GAAW,aAAa,KAAK;IAC3C,OAAO,oBAAoB,SAAS,OAAO;IAC3C,QAAQ,OAAO;GACjB;GAEA,MAAM,gBAAgB,OAAO,SAAS;GAEtC,QAAQ,KAAK,MAAM;GACnB,OAAO,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;GAExD,IAAI,OAAO,SAAS,SAAS,GAC3B,QAAQ,iBAAiB,OAAO,SAAS,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;EAEtE,CAAC;CACH;CAEA,MAAM,gBAAgB;EACpB,MAAM,OAAO,QAAQ;EAErB,IAAI,SAAS,KAAA,GACX;OAGA,KAAK,UAAU;CAEnB;CAEA,OAAO;EAAE;EAAS;CAAQ;AAC5B;AAEA,MAAM,gBAAgB;AAEtB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;AA0BtB,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkJzB,MAAM,cAAc;;AAGpB,MAAM,cAAc,UAClB,KAAK,UAAU,KAAK,EAAE,QAAQ,oBAAmB,SAC/C,SAAS,WAAW,YAAY,SAClC;AAEF,MAAM,eAAe,UAA+C;CAElE,MAAM,SAAS,mBADD,MAAM,KAAI,SAAQ,KAAK,IACC,CAAC;CAQvC,OAAO,QAAQ,iBAAiB,UAAU,WAL5B,MAAM,KAAK,MAAM,UAAU,CACvC,KAAK,MACL,KAAK,aAAa,OAAO,UAAU,SAAS,KAAK,UAAU,KAAK,IAAI,EAAE,EACxE,CAEyD,CAAC,EAAE;AAC9D;AAEA,MAAM,kBAAkB;;AAGxB,MAAM,mBAAmB,UAAkB,KAAK,MAAM,eAAe,GAAG,UAAU,KAAK;AAEvF,MAAM,cAAc;;AAGpB,MAAM,wBAAwB,OAAe,gBAC3C,MACG,MAAM,IAAI,EACV,QAAO,SAAQ;CACd,MAAM,QAAQ,YAAY,KAAK,IAAI;CAEnC,OAAO,UAAU,QAAQ,OAAO,MAAM,EAAE,KAAK;AAC/C,CAAC,EACA,KAAK,IAAI;AAEd,MAAM,gBAAgB,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,CAAC;AAE3D,MAAM,aAAa,UACjB,OAAO,eAAe,OAAO,IAAI,OAAO,oBAAoB,aAAa,EAAE,KAAK,IAAG,MAAK,EAAE,IAAI,CAAC;AAEjG,MAAM,kBAAkB,UAAiB;CACvC,MAAM,QAAQ,sCAAsC,KAAK,MAAM,SAAS,EAAE;CAE1E,IAAI,UAAU,MAAM,OAAO;CAE3B,MAAM,OAAO,OAAO,MAAM,EAAE,IAAI;CAEhC,OAAO,QAAQ,IAAI,UAAU,KAAK,KAAK,MAAM,MAAM,GAAG,IAAI,MAAM,MAAM,OAAO;AAC/E;AAEA,MAAM,oBAAoB,UAAmB;CAC3C,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;CAErE,IAAI,UAAU,KAAK,MAAM,qCACvB,OAAO,GAAG,QAAQ;CAKpB,OAAO,mCAAmC,UAFzB,iBAAiB,QAAQ,eAAe,KAAK,IAAI;AAGpE;;;;;;AAWA,MAAM,cAAc,SAA2B;CAC7C,IAAI,CAAC,UAAU,WAAW,WAAW,oBAAoB,GAAG,OAAO;EAAE,IAAI;EAAM;CAAK;CAEpF,IAAI;EACF,MAAM,WAAW,WAAW,qBAAqB,GAAG,gBAAgB,OAAO,iBAAiB,EAC1F,MAAM,QACR,CAAC;EAED,IAAI,CAAC,SAAS,WAAW,aAAa,KAAK,CAAC,SAAS,SAAS,aAAa,GACzE,OAAO;GAAE,IAAI;GAAO,OAAO;IAAE,MAAM;IAAU,SAAS;GAAkC;EAAE;EAG5F,OAAO;GACL,IAAI;GACJ,MAAM,SAAS,MAAM,IAAsB,SAAS,SAAS,CAAoB;EACnF;CACF,SAAS,OAAO;EACd,OAAO;GAAE,IAAI;GAAO,OAAO;IAAE,MAAM;IAAU,SAAS,iBAAiB,KAAK;GAAE;EAAE;CAClF;AACF;AAEA,MAAM,UAAU,SAA6C;CAC3D,MAAM,YAA0B;EAC9B,MAAM,KAAK;EACX,UAAU,MAAM,YAAY,KAAK,QAAQ,MAAM,OAAO;CACxD;CAEA,IAAI,KAAK,gBAAgB,KAAA,GAAW,UAAU,cAAc,KAAK;CAEjE,IAAI,KAAK,gBAAgB,KAAA,GAAW,UAAU,cAAc,KAAK;CAEjE,IAAI,KAAK,iBAAiB,KAAA,GAAW,UAAU,eAAe,KAAK;CAEnE,OAAO;AACT;AAEA,MAAM,YAAY,WAAiD;CACjE,MAAM,YAAY,OAAO,MAAM;CAE/B,IAAI,OAAO,WAAW,KAAA,GAAW,UAAU,SAAS,OAAO;CAE3D,IAAI,OAAO,cAAc,KAAA,GAAW,UAAU,YAAY,OAAO;CAEjE,OAAO;AACT;AAEA,MAAM,WAAW,WAAmD;CAClE,IAAI;CACJ;CACA,QAAQ,CAAC;AACX;AAEA,MAAM,cAAc,OAAO,OAAO;CAChC,KAAK,OAAO,OAAO,OAAO,QAAQ,OAAO,IAAI;CAC7C,QAAQ,OAAO,MAAM,OAAO,MAAM;AACpC,CAAC;AAED,MAAM,oBAAoB,OAAO,oBAAoB,WAAW;AAgBhE,MAAM,gBAAgB,QAAwB,gBAAiD;CAC7F,IAAI,OAAO,IAAI;EACb,MAAM,cAAc,kBAAkB,OAAO,WAAW;EAExD,IAAI,OAAO,OAAO,WAAW,GAC3B,OAAO,QAAQ;GACb,MAAM;GACN,SAAS;EACX,CAAC;EAGH,MAAM,YAAoC;GACxC,IAAI;GACJ,QAAQ,OAAO;GACf,aAAa,YAAY;EAC3B;EAEA,IAAI,OAAO,UAAU,KAAA,GAAW,UAAU,QAAQ,OAAO;EAEzD,OAAO;CACT;CAEA,MAAM,EAAE,MAAM,MAAM,SAAS,UAAU,OAAO;CAG9C,MAAM,QAA8B;EAClC;EACA,SAJY,SAAS,KAAA,KAAa,KAAK,SAAS,KAAK,CAAC,QAAQ,WAAW,IAAI,IAI5D,GAAG,KAAK,IAAI,YAAY;CAC3C;CAEA,IAAI,UAAU,KAAA,GAAW,MAAM,QAAQ,qBAAqB,OAAO,WAAW;CAE9E,OAAO;EAAE,IAAI;EAAO;EAAO,QAAQ,OAAO;CAAO;AACnD;AAEA,MAAM,iBAAiB,UAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;;;;AAchG,MAAa,0BACX,UAAqC,CAAC,MACjB;CACrB,MAAM,QAAQ,mBACZ,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,2BAAA,CAAyD,CAAC,CAC3F;CAEA,MAAM,MAAM,OAAO,MAAc,WAAmC,aAAqB;EACvF,MAAM,kBAAkB,UAAU,aAAa,KAAK,IAAI,IAAI;EAC5D,MAAM,WAAW,WAAW,IAAI;EAEhC,IAAI,CAAC,SAAS,IAAI,OAAO,QAAQ,SAAS,KAAK;EAE/C,MAAM,YAAY,UAAU;EAE5B,IAAI,aAAa,GACf,OAAO,QAAQ;GACb,MAAM;GACN,SAAS,6BAA6B,UAAU,UAAU;EAC5D,CAAC;EAGH,MAAM,iBAAyC;GAC7C,OAAO,UAAU,MAAM,IAAI,MAAM;GACjC,SAAS,UAAU,QAAQ,IAAI,QAAQ;GACvC;GACA,kBAAkB,UAAU;EAC9B;EAEA,IAAI,QAAQ,SAAS,KAAA,GAAW,eAAe,OAAO,QAAQ;EAE9D,IAAI,QAAQ,cAAc,KAAA,GAAW,eAAe,YAAY,QAAQ;EAExE,IAAI;EAEJ,IAAI;GACF,UAAU,IAAI,gBAAgB,cAAc;EAC9C,SAAS,OAAO;GACd,OAAO,QAAQ;IACb,MAAM;IACN,SAAS,iCAAiC,cAAc,KAAK;GAC/D,CAAC;EACH;EAEA,IAAI;GACF,OAAO,aACL,MAAM,QAAQ,QAAQ,GAAG,cAAc,SAAS,OAAO,YAAY,UAAU,KAAK,KAAK;IACrF,QAAQ,UAAU;IAClB,OAAO,UAAU;IACjB;GACF,CAAC,GACD,gBAAgB,SAAS,IAAI,CAC/B;EACF,UAAU;GACR,MAAM,QAAQ,MAAM;EACtB;CACF;CAEA,OAAO,EACL,SAAS,OAAO,MAAM,cAAc;EAClC,MAAM,WAAW,KAAK,IAAI;EAC1B,MAAM,OAAO,MAAM,MAAM,QAAQ,UAAU,QAAQ,UAAU,SAAS;EAEtE,IAAI,SAAS,WAAW,OAAO,QAAQ;GAAE,MAAM;GAAW,SAAS;EAAoB,CAAC;EAExF,IAAI,SAAS,WACX,OAAO,QAAQ;GACb,MAAM;GACN,SAAS,6BAA6B,UAAU,UAAU;EAC5D,CAAC;EAGH,IAAI;GACF,OAAO,MAAM,IAAI,MAAM,WAAW,QAAQ;EAC5C,UAAU;GACR,MAAM,QAAQ;EAChB;CACF,EACF;AACF"}
1
+ {"version":3,"file":"node.mjs","names":[],"sources":["../src/node.ts"],"sourcesContent":["import * as nodeModule from 'node:module'\nimport { Option, Predicate } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n CodemodeSandbox,\n type CodemodeResult,\n type CodemodeSandboxOptions,\n type CodemodeTool,\n type CodemodeWasmModule\n} from '@earendil-works/pi-codemode'\nimport { codeModeCallLabels } from './catalog.ts'\nimport type {\n CodeModeError,\n CodeModeExecuteOptions,\n CodeModeExecutionResult,\n CodeModeExecutor,\n CodeModeExecutorGlobal,\n CodeModeExecutorTool,\n CodeModeStoreWrites\n} from './executor.ts'\n\n/** Default cap of concurrent script executions per executor. */\nexport const defaultMaxConcurrentExecutions = 4\n\nexport type PiCodeModeExecutorOptions = {\n /**\n * Compiled `quickjs-wasi/quickjs.wasm` (see pi's `loadQuickJSWasm`). Default: the file of the\n * installed `quickjs-wasi` package, compiled once per process.\n */\n readonly wasm?: CodemodeWasmModule | Promise<CodemodeWasmModule>\n /**\n * Worker entry that imports `@earendil-works/pi-codemode/worker`. Default: pi's own worker file,\n * which exists on disk when the package is not bundled (Next `serverExternalPackages`).\n */\n readonly workerUrl?: string | URL\n /** Concurrent executions of this executor; more wait for a free slot. Default 4. */\n readonly maxConcurrentExecutions?: number\n}\n\ntype SlotOutcome = 'acquired' | 'aborted' | 'timeout'\n\ntype Waiter = (outcome: SlotOutcome) => void\n\n/** Abort- and deadline-aware counting semaphore for executions. */\nconst makeExecutionSlots = (max: number) => {\n let active = 0\n const waiting: Array<Waiter> = []\n\n const acquire = (signal: AbortSignal, timeoutMs: number): Promise<SlotOutcome> => {\n if (signal.aborted) return Promise.resolve('aborted')\n\n if (active < max) {\n active++\n\n return Promise.resolve('acquired')\n }\n\n return new Promise(resolve => {\n let timer: ReturnType<typeof setTimeout> | undefined\n\n const settle: Waiter = outcome => {\n const index = waiting.indexOf(settle)\n\n if (index !== -1) waiting.splice(index, 1)\n\n if (timer !== undefined) clearTimeout(timer)\n signal.removeEventListener('abort', onAbort)\n resolve(outcome)\n }\n\n const onAbort = () => settle('aborted')\n\n waiting.push(settle)\n signal.addEventListener('abort', onAbort, { once: true })\n\n if (Number.isFinite(timeoutMs)) {\n timer = setTimeout(() => settle('timeout'), Math.max(0, timeoutMs))\n }\n })\n }\n\n const release = () => {\n const next = waiting[0]\n\n if (next === undefined) {\n active--\n } else {\n // The slot passes straight to the next waiter.\n next('acquired')\n }\n }\n\n return { acquire, release }\n}\n\nconst wrapperPrefix = 'async function __yolkCodeMode() {\\n'\n\nconst wrapperSuffix = '\\n}'\n\n/** Most values one argument may hold; a larger argument is rejected. */\nconst maxArgumentValues = 100_000\n\n/** Deepest object/array nesting one argument may have; deeper arguments are rejected. */\nconst maxArgumentDepth = 64\n\n/**\n * Evaluated in the VM before the script runs: `(tools, labels) => guardedTools`, with `labels` the\n * `[name, callLabel]` pairs of the tools in order (`codeModeCallLabels`). Each call walks its first\n * argument once, the way `JSON.stringify` reads it (each own enumerable string key and each array\n * index read once, so getters run once; a callable `toJSON` is called once with the key, `''` at\n * the root, and its result is walked at the same path), and builds a fresh copy (null-prototype\n * objects and arrays) that is what the call sends. It rejects with a `TypeError` naming the call\n * label and path when JSON would change the value instead of carrying it: non-finite numbers,\n * invalid `Date`s, `undefined`/function/symbol array items and holes, function/symbol/bigint\n * values, cycles, and objects that are neither plain (prototype `Object.prototype` or `null`) nor\n * arrays and have no `toJSON`. An argument with more than `maxArgumentValues` values or nested more\n * than `maxArgumentDepth` levels is rejected too, never sent unchecked. `undefined`-valued keys are\n * left out (absent, as JSON drops them); the same object twice (not a cycle) is copied twice. An\n * error thrown by a getter or `toJSON` rejects the call with that error. A rejected call never\n * reaches the host. Every member of pi's `tools` is wrapped; unknown members fall through to pi's\n * proxy (close-match suggestions). Not a security boundary (`globalThis.tools` is unguarded):\n * arguments are still decoded by the host.\n */\nconst toolsGuardSource = `(function (tools, labels, unbound) {\n // Captured before the script runs, so a script that changes built-ins cannot change the checks.\n const { apply, getPrototypeOf, setPrototypeOf } = Reflect\n const isFinite = Number.isFinite\n const isArray = Array.isArray\n const { keys, create, defineProperty } = Object\n const getTime = Date.prototype.getTime\n const TypeErrorCtor = TypeError\n const SetCtor = Set\n const setAdd = Set.prototype.add\n const setHas = Set.prototype.has\n const setDelete = Set.prototype.delete\n const ObjectProto = Object.prototype\n const { trunc, min } = Math\n const stringify = JSON.stringify\n const plainJson = 'pass plain JSON (objects, arrays, strings, finite numbers, booleans, null)'\n const maxValues = ${maxArgumentValues}\n const maxDepth = ${maxArgumentDepth}\n class Rejected {\n constructor(message) {\n defineProperty(this, 'message', { value: message })\n }\n }\n const keyPath = (path, key) =>\n /^[A-Za-z_$][\\\\w$]*$/.test(key)\n ? (path === '' ? key : path + '.' + key)\n : path + '[' + stringify(key) + ']'\n const typeName = value => {\n const proto = getPrototypeOf(value)\n const name = proto && typeof proto.constructor === 'function' ? proto.constructor.name : ''\n return name ? (/^[AEIOU]/.test(name) ? 'an ' : 'a ') + name : 'a non-plain object'\n }\n // The JSON copy of a value already read from its holder at key (undefined: absent), or throws\n // Rejected with why JSON would not carry it unchanged.\n const snapshot = (read, key, path, slot, walk) => {\n const at = path === '' ? 'is ' : 'at ' + path + ' is '\n if (++walk.values > maxValues) {\n throw new Rejected('has more than ' + maxValues + ' values; split the work across calls')\n }\n let value = read\n const kind = typeof value\n if ((kind === 'object' && value !== null) || kind === 'function' || kind === 'bigint') {\n // The intrinsic getTime reads a Date's internal time (and throws for anything else).\n let time\n try {\n time = apply(getTime, value, [])\n } catch {\n time = undefined\n }\n if (time !== undefined && !isFinite(time)) {\n throw new Rejected(at + 'an invalid Date; pass a valid Date or an ISO string')\n }\n const toJSON = value.toJSON\n if (typeof toJSON === 'function') value = apply(toJSON, value, [key])\n }\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return value\n case 'number':\n if (isFinite(value)) return value\n throw new Rejected(\n at + value + '; pass a finite number' + (slot === 'key' ? ' or omit the key' : '')\n )\n case 'undefined':\n if (slot === 'item') throw new Rejected(at + 'undefined; arrays cannot hold undefined')\n return undefined\n case 'bigint':\n case 'function':\n case 'symbol':\n throw new Rejected(at + 'a ' + typeof value + '; ' + plainJson)\n }\n if (value === null) return null\n if (apply(setHas, walk.ancestors, [value])) {\n throw new Rejected(at + 'a circular reference; ' + plainJson)\n }\n if (walk.depth >= maxDepth) {\n throw new Rejected('at ' + path + ' is nested more than ' + maxDepth + ' levels deep')\n }\n const array = isArray(value)\n if (!array) {\n const proto = getPrototypeOf(value)\n if (proto !== ObjectProto && proto !== null) {\n throw new Rejected(at + typeName(value) + '; ' + plainJson)\n }\n }\n apply(setAdd, walk.ancestors, [value])\n walk.depth++\n let copy\n if (array) {\n // LengthOfArrayLike, read once, as JSON.stringify does.\n // Unary plus is ToNumber: one coercion, and it throws for a bigint as JSON.stringify does.\n const raw = +value.length\n const length = raw > 0 ? min(trunc(raw), 9007199254740991) : 0\n // No prototype: no inherited toJSON or index setter can change the checked copy when pi\n // serializes it (still an array to JSON.stringify).\n copy = []\n setPrototypeOf(copy, null)\n for (let i = 0; i < length; i++) {\n copy[i] = snapshot(value[i], '' + i, path + '[' + i + ']', 'item', walk)\n }\n } else {\n copy = create(null)\n // Indexed over the fresh key array: no iterator a script could replace.\n const names = keys(value)\n for (let i = 0; i < names.length; i++) {\n const name = names[i]\n const item = snapshot(value[name], name, keyPath(path, name), 'key', walk)\n if (item !== undefined) copy[name] = item\n }\n }\n walk.depth--\n apply(setDelete, walk.ancestors, [value])\n return copy\n }\n const wrap = (label, call) => async (...args) => {\n let json\n try {\n json = snapshot(args[0], '', '', 'root', { values: 0, depth: 0, ancestors: new SetCtor() })\n } catch (error) {\n if (error instanceof Rejected) throw new TypeErrorCtor(label + ': argument ' + error.message)\n throw error\n }\n return call(json)\n }\n // Each tool's function sits at tools[name] unless an earlier tool holds that key, in which case\n // that earlier function is already wrapped.\n const wrappers = new Map()\n for (const [name, label] of labels) {\n const call = name in tools ? tools[name] : undefined\n if (typeof call === 'function' && !wrappers.has(call)) wrappers.set(call, wrap(label, call))\n }\n const guarded = Object.create(null)\n for (const key of Object.keys(tools)) {\n const call = tools[key]\n if (!wrappers.has(call)) wrappers.set(call, wrap('tools[' + JSON.stringify(key) + ']', call))\n guarded[key] = wrappers.get(call)\n }\n // Unbound keys (stage-only tools) reject with their hint instead of being undefined.\n const rejections = create(null)\n for (const [key, message] of unbound) {\n if (!(key in guarded)) rejections[key] = async () => { throw new TypeErrorCtor(message) }\n }\n return new Proxy(Object.freeze(guarded), {\n get: (target, key) => (key in target ? target[key] : key in rejections ? rejections[key] : tools[key])\n })\n})`\n\n// pi runs `(async (tools, console) => {<code>\\n})`. The script runs in an inner function of the\n// same shape whose `tools` is guarded; the prefix shares line 1 with the script (only line-1\n// columns shift) and the guard follows the script, so its line numbers are unchanged.\nconst guardPrefix = 'return (async (tools, console) => {'\n\n/** JSON as a JavaScript expression (U+2028/U+2029 escaped for older parsers). */\nconst jsonSource = (value: unknown) =>\n JSON.stringify(value).replace(/[\\u2028\\u2029]/g, char =>\n char === '\\u2028' ? '\\\\u2028' : '\\\\u2029'\n )\n\nconst guardSuffix = (\n tools: ReadonlyArray<CodeModeExecutorTool>,\n unbound: CodeModeExecuteOptions['unbound']\n) => {\n const names = tools.map(tool => tool.name)\n const labels = codeModeCallLabels(names)\n\n // `makeCodeModeTool` supplies each label; other callers get pi's binding rule.\n const pairs = tools.map((tool, index) => [\n tool.name,\n tool.callLabel ?? labels[index] ?? `tools[${JSON.stringify(tool.name)}]`\n ])\n\n const rejected = (unbound ?? []).map(entry => [entry.key, entry.message])\n\n return `\\n})(${toolsGuardSource}(tools, ${jsonSource(pairs)}, ${jsonSource(rejected)}), console)`\n}\n\nconst lineTerminators = /\\r\\n?|[\\n\\u2028\\u2029]/g\n\n/** Lines of the script as QuickJS counts them (at least; never fewer). */\nconst scriptLineCount = (code: string) => (code.match(lineTerminators)?.length ?? 0) + 1\n\nconst scriptFrame = /codemode\\.js:(\\d+):\\d+/\n\n/** Drops stack frames of the wrapper and guard, which sit below the script's last line. */\nconst withoutWrapperFrames = (stack: string, scriptLines: number) =>\n stack\n .split('\\n')\n .filter(line => {\n const frame = scriptFrame.exec(line)\n\n return frame === null || Number(frame[1]) <= scriptLines\n })\n .join('\\n')\n\nconst ErrorWithCode = Schema.Struct({ code: Schema.String })\n\nconst errorCode = (error: unknown) =>\n Option.getOrUndefined(Option.map(Schema.decodeUnknownOption(ErrorWithCode)(error), e => e.code))\n\nconst syntaxLocation = (error: Error) => {\n const match = /^[^\\n]*:(\\d+)\\n([^\\n]*)\\n([^\\n]*)\\n/.exec(error.stack ?? '')\n\n if (match === null) return ''\n\n const line = Number(match[1]) - 1\n\n return line >= 1 ? ` (line ${line})\\n${match[2] ?? ''}\\n${match[3] ?? ''}` : ''\n}\n\nconst typeStripMessage = (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n\n if (errorCode(error) === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX') {\n return `${message}. Write plain JavaScript: type annotations are stripped, but enums, namespaces, and parameter properties are not supported.`\n }\n\n const location = error instanceof Error ? syntaxLocation(error) : ''\n\n return `The script could not be parsed: ${message}${location}`\n}\n\ntype Stripped =\n | { readonly ok: true; readonly code: string }\n | { readonly ok: false; readonly error: CodeModeError }\n\n/**\n * Strips TypeScript annotations with Node's `stripTypeScriptTypes` (Node 22.13+; whitespace\n * replacement keeps line and column numbers). The script is wrapped in an async function so\n * top-level `return` and `await` parse. On Node without the API, code passes through unchanged.\n */\nconst stripTypes = (code: string): Stripped => {\n if (!Predicate.isFunction(nodeModule.stripTypeScriptTypes)) return { ok: true, code }\n\n try {\n const stripped = nodeModule.stripTypeScriptTypes(`${wrapperPrefix}${code}${wrapperSuffix}`, {\n mode: 'strip'\n })\n\n if (!stripped.startsWith(wrapperPrefix) || !stripped.endsWith(wrapperSuffix)) {\n return { ok: false, error: { kind: 'script', message: 'The script could not be parsed.' } }\n }\n\n return {\n ok: true,\n code: stripped.slice(wrapperPrefix.length, stripped.length - wrapperSuffix.length)\n }\n } catch (error) {\n return { ok: false, error: { kind: 'script', message: typeStripMessage(error) } }\n }\n}\n\nconst piTool = (tool: CodeModeExecutorTool): CodemodeTool => {\n const converted: CodemodeTool = {\n name: tool.name,\n execute: (args, context) => tool.execute(args, context)\n }\n\n if (tool.description !== undefined) converted.description = tool.description\n\n if (tool.inputSchema !== undefined) converted.inputSchema = tool.inputSchema\n\n if (tool.outputSchema !== undefined) converted.outputSchema = tool.outputSchema\n\n return converted\n}\n\nconst piGlobal = (global: CodeModeExecutorGlobal): CodemodeTool => {\n const converted = piTool(global)\n\n if (global.spread !== undefined) converted.spread = global.spread\n\n if (global.signature !== undefined) converted.signature = global.signature\n\n return converted\n}\n\nconst failure = (error: CodeModeError): CodeModeExecutionResult => ({\n ok: false,\n error,\n output: []\n})\n\nconst StoreWrites = Schema.Struct({\n set: Schema.Record(Schema.String, Schema.Json),\n delete: Schema.Array(Schema.String)\n})\n\nconst decodeStoreWrites = Schema.decodeUnknownOption(StoreWrites)\n\ntype MutableCodeModeError = {\n kind: CodeModeError['kind']\n message: string\n stack?: string\n}\n\ntype MutableExecutionResult = {\n ok: boolean\n value?: unknown\n error?: CodeModeError\n output: CodeModeExecutionResult['output']\n storeWrites?: CodeModeStoreWrites\n}\n\nconst fromPiResult = (result: CodemodeResult, scriptLines: number): CodeModeExecutionResult => {\n if (result.ok) {\n const storeWrites = decodeStoreWrites(result.storeWrites)\n\n if (Option.isNone(storeWrites)) {\n return failure({\n kind: 'sandbox',\n message: 'The script reported store writes that are not JSON.'\n })\n }\n\n const converted: MutableExecutionResult = {\n ok: true,\n output: result.output,\n storeWrites: storeWrites.value\n }\n\n if (result.value !== undefined) converted.value = result.value\n\n return converted\n }\n\n const { kind, name, message, stack } = result.error\n const named = name !== undefined && name.length > 0 && !message.startsWith(name)\n\n const error: MutableCodeModeError = {\n kind,\n message: named ? `${name}: ${message}` : message\n }\n\n if (stack !== undefined) error.stack = withoutWrapperFrames(stack, scriptLines)\n\n return { ok: false, error, output: result.output }\n}\n\nconst describeError = (error: unknown) => (error instanceof Error ? error.message : String(error))\n\n/**\n * A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM\n * (WebAssembly) in a fresh worker thread, closed when the execution ends. Applies the timeout,\n * heap cap, abort signal, and store, strips TypeScript annotations first, rejects tool arguments\n * that JSON would silently change inside the script (see `CodeModeExecutorTool`), and caps concurrent\n * executions per executor; create one executor per process (module scope) so the cap is\n * per process.\n *\n * Node only. The worker inherits a copy of `process.env` (scripts cannot read it: the VM has no\n * `process`). Under Next.js, list `@yolk-sdk/codemode`, `@earendil-works/pi-codemode`, and\n * `quickjs-wasi` in `serverExternalPackages` so the worker file and wasm stay on disk.\n */\nexport const makePiCodeModeExecutor = (\n options: PiCodeModeExecutorOptions = {}\n): CodeModeExecutor => {\n const slots = makeExecutionSlots(\n Math.max(1, Math.floor(options.maxConcurrentExecutions ?? defaultMaxConcurrentExecutions))\n )\n\n const run = async (code: string, execution: CodeModeExecuteOptions, queuedAt: number) => {\n const remaining = () => execution.timeoutMs - (Date.now() - queuedAt)\n const stripped = stripTypes(code)\n\n if (!stripped.ok) return failure(stripped.error)\n\n const timeoutMs = remaining()\n\n if (timeoutMs <= 0) {\n return failure({\n kind: 'timeout',\n message: `Execution timed out after ${execution.timeoutMs} ms`\n })\n }\n\n const sandboxOptions: CodemodeSandboxOptions = {\n tools: execution.tools.map(piTool),\n globals: execution.globals.map(piGlobal),\n timeoutMs,\n memoryLimitBytes: execution.memoryLimitBytes\n }\n\n if (options.wasm !== undefined) sandboxOptions.wasm = options.wasm\n\n if (options.workerUrl !== undefined) sandboxOptions.workerUrl = options.workerUrl\n\n let sandbox: CodemodeSandbox\n\n try {\n sandbox = new CodemodeSandbox(sandboxOptions)\n } catch (error) {\n return failure({\n kind: 'sandbox',\n message: `Could not create the sandbox: ${describeError(error)}`\n })\n }\n\n try {\n return fromPiResult(\n await sandbox.execute(\n `${guardPrefix}${stripped.code}${guardSuffix(execution.tools, execution.unbound)}`,\n {\n signal: execution.signal,\n store: execution.store,\n timeoutMs\n }\n ),\n scriptLineCount(stripped.code)\n )\n } finally {\n await sandbox.close()\n }\n }\n\n return {\n execute: async (code, execution) => {\n const queuedAt = Date.now()\n const slot = await slots.acquire(execution.signal, execution.timeoutMs)\n\n if (slot === 'aborted') return failure({ kind: 'aborted', message: 'Execution aborted' })\n\n if (slot === 'timeout') {\n return failure({\n kind: 'timeout',\n message: `Execution timed out after ${execution.timeoutMs} ms while waiting for a free execution slot`\n })\n }\n\n try {\n return await run(code, execution, queuedAt)\n } finally {\n slots.release()\n }\n }\n }\n}\n"],"mappings":"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"}
package/dist/search.d.mts CHANGED
@@ -3,21 +3,26 @@ import { CodeModeCatalogTool } from "./catalog.mjs";
3
3
  //#region src/search.d.ts
4
4
  /** Default number of `searchTools` results. */
5
5
  declare const defaultCodeModeSearchLimit = 8;
6
- /** Lowercase word tokens; splits on non-alphanumerics and camelCase boundaries. */
7
- declare const codeModeSearchTokens: (text: string) => ReadonlyArray<string>;
8
6
  type CodeModeSearchOptions = {
9
7
  readonly limit?: number;
10
8
  readonly namespace?: string;
11
9
  };
12
10
  type CodeModeSearchHit = {
13
- /** The identifier scripts call as `tools.<name>(args)`. */readonly name: string;
14
- readonly description: string;
11
+ /** The identifier scripts call as `tools.<name>(args)`, or for a stage-only tool the name
12
+ * scripts pass to `stage(name, args)`.
13
+ */
14
+ readonly name: string;
15
+ readonly description: string; /** A stage-only tool (ADR 0007): no `tools` binding, only `stage()` it. */
16
+ readonly stageOnly?: true;
15
17
  };
16
18
  /**
17
- * BM25 over each tool's identifier, raw name, description, namespace, and namespace description. Ties keep catalog order.
18
- * A query without words lists the (namespace-filtered) tools in catalog order.
19
+ * BM25 (the shared `rankToolSearchDocuments`) over each tool's identifier, raw name, description,
20
+ * namespace, and namespace description. Ties keep catalog order. A query without words lists the
21
+ * (namespace-filtered) tools in catalog order.
19
22
  */
20
23
  declare const searchCodeModeTools: (catalog: ReadonlyArray<CodeModeCatalogTool>, query: string, options?: CodeModeSearchOptions) => ReadonlyArray<CodeModeSearchHit>;
24
+ /** Lowercase word tokens of code mode search (the shared `toolSearchTokens`). */
25
+ declare const codeModeSearchTokens: (text: string) => ReadonlyArray<string>;
21
26
  //#endregion
22
27
  export { CodeModeSearchHit, CodeModeSearchOptions, codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools };
23
28
  //# sourceMappingURL=search.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.mts","names":[],"sources":["../src/search.ts"],"mappings":";;;;cAGa,0BAAA;AAAb;AAAA,cAOa,oBAAA,GAAwB,IAAA,aAAe,aAAa;AAAA,KAiCrD,qBAAA;EAAA,SACD,KAAA;EAAA,SACA,SAAS;AAAA;AAAA,KAGR,iBAAA;sEAED,IAAA;EAAA,SACA,WAAW;AAAA;;;;AANF;cAaP,mBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,KAAA,UACA,OAAA,GAAS,qBAAA,KACR,aAAA,CAAc,iBAAA"}
1
+ {"version":3,"file":"search.d.mts","names":[],"sources":["../src/search.ts"],"mappings":";;;;cAIa,0BAAA;AAAA,KAED,qBAAA;EAAA,SACD,KAAA;EAAA,SACA,SAAS;AAAA;AAAA,KAGR,iBAAA;EALA;;;EAAA,SASD,IAAA;EAAA,SACA,WAAA,UALC;EAAA,SAOD,SAAA;AAAA;;;;;;cAQE,mBAAA,GACX,OAAA,EAAS,aAAA,CAAc,mBAAA,GACvB,KAAA,UACA,OAAA,GAAS,qBAAA,KACR,aAAA,CAAc,iBAAA;AAJjB;AAAA,cAgCa,oBAAA,GAAoB,IAAA,aAAA,aAAA"}
package/dist/search.mjs CHANGED
@@ -1,57 +1,35 @@
1
+ import { rankToolSearchDocuments, toolSearchTokens } from "@yolk-sdk/agent/tools";
1
2
  //#region src/search.ts
2
3
  /** Default number of `searchTools` results. */
3
4
  const defaultCodeModeSearchLimit = 8;
4
- const k1 = 1.2;
5
- const b = .75;
6
- /** Lowercase word tokens; splits on non-alphanumerics and camelCase boundaries. */
7
- const codeModeSearchTokens = (text) => text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((token) => token.length > 0);
8
- const indexTool = (tool) => {
9
- const tokens = codeModeSearchTokens([
10
- tool.identifier,
11
- tool.name,
12
- tool.description,
13
- tool.namespace,
14
- tool.namespaceDescription ?? ""
15
- ].join(" "));
16
- const frequencies = /* @__PURE__ */ new Map();
17
- for (const token of tokens) frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
18
- return {
19
- tool,
20
- frequencies,
21
- length: tokens.length
22
- };
23
- };
24
5
  /**
25
- * BM25 over each tool's identifier, raw name, description, namespace, and namespace description. Ties keep catalog order.
26
- * A query without words lists the (namespace-filtered) tools in catalog order.
6
+ * BM25 (the shared `rankToolSearchDocuments`) over each tool's identifier, raw name, description,
7
+ * namespace, and namespace description. Ties keep catalog order. A query without words lists the
8
+ * (namespace-filtered) tools in catalog order.
27
9
  */
28
10
  const searchCodeModeTools = (catalog, query, options = {}) => {
29
- const limit = Math.max(0, Math.floor(options.limit ?? 8));
30
- const documents = (options.namespace === void 0 ? catalog : catalog.filter((tool) => tool.namespace === options.namespace)).map(indexTool);
31
- const terms = [...new Set(codeModeSearchTokens(query))];
32
- const hit = (tool) => ({
11
+ const scoped = options.namespace === void 0 ? catalog : catalog.filter((tool) => tool.namespace === options.namespace);
12
+ const hit = (tool) => tool.exposure === "stage" ? {
13
+ name: tool.name,
14
+ description: tool.description,
15
+ stageOnly: true
16
+ } : {
33
17
  name: tool.identifier,
34
18
  description: tool.description
35
- });
36
- if (terms.length === 0) return documents.slice(0, limit).map((document) => hit(document.tool));
37
- const averageLength = documents.reduce((total, document) => total + document.length, 0) / Math.max(1, documents.length);
38
- const inverse = new Map(terms.map((term) => {
39
- const containing = documents.filter((document) => document.frequencies.has(term)).length;
40
- return [term, Math.log(1 + (documents.length - containing + .5) / (containing + .5))];
41
- }));
42
- return documents.map((document, index) => {
43
- return {
44
- document,
45
- index,
46
- score: terms.reduce((total, term) => {
47
- const frequency = document.frequencies.get(term) ?? 0;
48
- if (frequency === 0) return total;
49
- const norm = k1 * (1 - b + b * document.length / Math.max(1, averageLength));
50
- return total + (inverse.get(term) ?? 0) * frequency * (k1 + 1) / (frequency + norm);
51
- }, 0)
52
- };
53
- }).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.index - right.index).slice(0, limit).map((entry) => hit(entry.document.tool));
19
+ };
20
+ return rankToolSearchDocuments(scoped.map((tool) => ({
21
+ value: tool,
22
+ text: [
23
+ tool.identifier,
24
+ tool.name,
25
+ tool.description,
26
+ tool.namespace,
27
+ tool.namespaceDescription ?? ""
28
+ ].join(" ")
29
+ })), query, options.limit ?? 8).map(hit);
54
30
  };
31
+ /** Lowercase word tokens of code mode search (the shared `toolSearchTokens`). */
32
+ const codeModeSearchTokens = toolSearchTokens;
55
33
  //#endregion
56
34
  export { codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools };
57
35
 
@@ -1 +1 @@
1
- {"version":3,"file":"search.mjs","names":[],"sources":["../src/search.ts"],"sourcesContent":["import type { CodeModeCatalogTool } from './catalog.ts'\n\n/** Default number of `searchTools` results. */\nexport const defaultCodeModeSearchLimit = 8\n\nconst k1 = 1.2\n\nconst b = 0.75\n\n/** Lowercase word tokens; splits on non-alphanumerics and camelCase boundaries. */\nexport const codeModeSearchTokens = (text: string): ReadonlyArray<string> =>\n text\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter(token => token.length > 0)\n\ntype IndexedTool = {\n readonly tool: CodeModeCatalogTool\n readonly frequencies: ReadonlyMap<string, number>\n readonly length: number\n}\n\nconst indexTool = (tool: CodeModeCatalogTool): IndexedTool => {\n const tokens = codeModeSearchTokens(\n [\n tool.identifier,\n tool.name,\n tool.description,\n tool.namespace,\n tool.namespaceDescription ?? ''\n ].join(' ')\n )\n\n const frequencies = new Map<string, number>()\n\n for (const token of tokens) {\n frequencies.set(token, (frequencies.get(token) ?? 0) + 1)\n }\n\n return { tool, frequencies, length: tokens.length }\n}\n\nexport type CodeModeSearchOptions = {\n readonly limit?: number\n readonly namespace?: string\n}\n\nexport type CodeModeSearchHit = {\n /** The identifier scripts call as `tools.<name>(args)`. */\n readonly name: string\n readonly description: string\n}\n\n/**\n * BM25 over each tool's identifier, raw name, description, namespace, and namespace description. Ties keep catalog order.\n * A query without words lists the (namespace-filtered) tools in catalog order.\n */\nexport const searchCodeModeTools = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n query: string,\n options: CodeModeSearchOptions = {}\n): ReadonlyArray<CodeModeSearchHit> => {\n const limit = Math.max(0, Math.floor(options.limit ?? defaultCodeModeSearchLimit))\n\n const scoped =\n options.namespace === undefined\n ? catalog\n : catalog.filter(tool => tool.namespace === options.namespace)\n\n const documents = scoped.map(indexTool)\n const terms = [...new Set(codeModeSearchTokens(query))]\n\n const hit = (tool: CodeModeCatalogTool): CodeModeSearchHit => ({\n name: tool.identifier,\n description: tool.description\n })\n\n if (terms.length === 0) {\n return documents.slice(0, limit).map(document => hit(document.tool))\n }\n\n const averageLength =\n documents.reduce((total, document) => total + document.length, 0) /\n Math.max(1, documents.length)\n\n const inverse = new Map(\n terms.map((term): readonly [string, number] => {\n const containing = documents.filter(document => document.frequencies.has(term)).length\n\n return [term, Math.log(1 + (documents.length - containing + 0.5) / (containing + 0.5))]\n })\n )\n\n return documents\n .map((document, index) => {\n const score = terms.reduce((total, term) => {\n const frequency = document.frequencies.get(term) ?? 0\n\n if (frequency === 0) return total\n\n const norm = k1 * (1 - b + (b * document.length) / Math.max(1, averageLength))\n\n return total + ((inverse.get(term) ?? 0) * frequency * (k1 + 1)) / (frequency + norm)\n }, 0)\n\n return { document, index, score }\n })\n .filter(entry => entry.score > 0)\n .sort((left, right) => right.score - left.score || left.index - right.index)\n .slice(0, limit)\n .map(entry => hit(entry.document.tool))\n}\n"],"mappings":";;AAGA,MAAa,6BAA6B;AAE1C,MAAM,KAAK;AAEX,MAAM,IAAI;;AAGV,MAAa,wBAAwB,SACnC,KACG,QAAQ,sBAAsB,OAAO,EACrC,YAAY,EACZ,MAAM,YAAY,EAClB,QAAO,UAAS,MAAM,SAAS,CAAC;AAQrC,MAAM,aAAa,SAA2C;CAC5D,MAAM,SAAS,qBACb;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,wBAAwB;CAC/B,EAAE,KAAK,GAAG,CACZ;CAEA,MAAM,8BAAc,IAAI,IAAoB;CAE5C,KAAK,MAAM,SAAS,QAClB,YAAY,IAAI,QAAQ,YAAY,IAAI,KAAK,KAAK,KAAK,CAAC;CAG1D,OAAO;EAAE;EAAM;EAAa,QAAQ,OAAO;CAAO;AACpD;;;;;AAiBA,MAAa,uBACX,SACA,OACA,UAAiC,CAAC,MACG;CACrC,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,SAAA,CAAmC,CAAC;CAOjF,MAAM,aAJJ,QAAQ,cAAc,KAAA,IAClB,UACA,QAAQ,QAAO,SAAQ,KAAK,cAAc,QAAQ,SAAS,GAExC,IAAI,SAAS;CACtC,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,qBAAqB,KAAK,CAAC,CAAC;CAEtD,MAAM,OAAO,UAAkD;EAC7D,MAAM,KAAK;EACX,aAAa,KAAK;CACpB;CAEA,IAAI,MAAM,WAAW,GACnB,OAAO,UAAU,MAAM,GAAG,KAAK,EAAE,KAAI,aAAY,IAAI,SAAS,IAAI,CAAC;CAGrE,MAAM,gBACJ,UAAU,QAAQ,OAAO,aAAa,QAAQ,SAAS,QAAQ,CAAC,IAChE,KAAK,IAAI,GAAG,UAAU,MAAM;CAE9B,MAAM,UAAU,IAAI,IAClB,MAAM,KAAK,SAAoC;EAC7C,MAAM,aAAa,UAAU,QAAO,aAAY,SAAS,YAAY,IAAI,IAAI,CAAC,EAAE;EAEhF,OAAO,CAAC,MAAM,KAAK,IAAI,KAAK,UAAU,SAAS,aAAa,OAAQ,aAAa,GAAI,CAAC;CACxF,CAAC,CACH;CAEA,OAAO,UACJ,KAAK,UAAU,UAAU;EAWxB,OAAO;GAAE;GAAU;GAAO,OAVZ,MAAM,QAAQ,OAAO,SAAS;IAC1C,MAAM,YAAY,SAAS,YAAY,IAAI,IAAI,KAAK;IAEpD,IAAI,cAAc,GAAG,OAAO;IAE5B,MAAM,OAAO,MAAM,IAAI,IAAK,IAAI,SAAS,SAAU,KAAK,IAAI,GAAG,aAAa;IAE5E,OAAO,SAAU,QAAQ,IAAI,IAAI,KAAK,KAAK,aAAa,KAAK,MAAO,YAAY;GAClF,GAAG,CAE2B;EAAE;CAClC,CAAC,EACA,QAAO,UAAS,MAAM,QAAQ,CAAC,EAC/B,MAAM,MAAM,UAAU,MAAM,QAAQ,KAAK,SAAS,KAAK,QAAQ,MAAM,KAAK,EAC1E,MAAM,GAAG,KAAK,EACd,KAAI,UAAS,IAAI,MAAM,SAAS,IAAI,CAAC;AAC1C"}
1
+ {"version":3,"file":"search.mjs","names":[],"sources":["../src/search.ts"],"sourcesContent":["import { rankToolSearchDocuments, toolSearchTokens } from '@yolk-sdk/agent/tools'\nimport type { CodeModeCatalogTool } from './catalog.ts'\n\n/** Default number of `searchTools` results. */\nexport const defaultCodeModeSearchLimit = 8\n\nexport type CodeModeSearchOptions = {\n readonly limit?: number\n readonly namespace?: string\n}\n\nexport type CodeModeSearchHit = {\n /** The identifier scripts call as `tools.<name>(args)`, or for a stage-only tool the name\n * scripts pass to `stage(name, args)`.\n */\n readonly name: string\n readonly description: string\n /** A stage-only tool (ADR 0007): no `tools` binding, only `stage()` it. */\n readonly stageOnly?: true\n}\n\n/**\n * BM25 (the shared `rankToolSearchDocuments`) over each tool's identifier, raw name, description,\n * namespace, and namespace description. Ties keep catalog order. A query without words lists the\n * (namespace-filtered) tools in catalog order.\n */\nexport const searchCodeModeTools = (\n catalog: ReadonlyArray<CodeModeCatalogTool>,\n query: string,\n options: CodeModeSearchOptions = {}\n): ReadonlyArray<CodeModeSearchHit> => {\n const scoped =\n options.namespace === undefined\n ? catalog\n : catalog.filter(tool => tool.namespace === options.namespace)\n\n const hit = (tool: CodeModeCatalogTool): CodeModeSearchHit =>\n tool.exposure === 'stage'\n ? { name: tool.name, description: tool.description, stageOnly: true }\n : { name: tool.identifier, description: tool.description }\n\n return rankToolSearchDocuments(\n scoped.map(tool => ({\n value: tool,\n text: [\n tool.identifier,\n tool.name,\n tool.description,\n tool.namespace,\n tool.namespaceDescription ?? ''\n ].join(' ')\n })),\n query,\n options.limit ?? defaultCodeModeSearchLimit\n ).map(hit)\n}\n\n/** Lowercase word tokens of code mode search (the shared `toolSearchTokens`). */\nexport const codeModeSearchTokens = toolSearchTokens\n"],"mappings":";;;AAIA,MAAa,6BAA6B;;;;;;AAsB1C,MAAa,uBACX,SACA,OACA,UAAiC,CAAC,MACG;CACrC,MAAM,SACJ,QAAQ,cAAc,KAAA,IAClB,UACA,QAAQ,QAAO,SAAQ,KAAK,cAAc,QAAQ,SAAS;CAEjE,MAAM,OAAO,SACX,KAAK,aAAa,UACd;EAAE,MAAM,KAAK;EAAM,aAAa,KAAK;EAAa,WAAW;CAAK,IAClE;EAAE,MAAM,KAAK;EAAY,aAAa,KAAK;CAAY;CAE7D,OAAO,wBACL,OAAO,KAAI,UAAS;EAClB,OAAO;EACP,MAAM;GACJ,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK;GACL,KAAK,wBAAwB;EAC/B,EAAE,KAAK,GAAG;CACZ,EAAE,GACF,OACA,QAAQ,SAAA,CACV,EAAE,IAAI,GAAG;AACX;;AAGA,MAAa,uBAAuB"}
package/dist/tool.d.mts CHANGED
@@ -78,6 +78,15 @@ type MakeCodeModeToolOptions<Context> = {
78
78
  * resolution's cap.
79
79
  */
80
80
  readonly staging?: CodeModeStagingOptions | false;
81
+ /**
82
+ * Where the nested tool catalog goes (ADR 0007). `description` (default): in this tool's
83
+ * description, which changes with the catalog. `instructions`: the description stays invariant
84
+ * and the catalog (every namespace with its tool count, listed tools within `inlineBudget`) is
85
+ * the resolution's `instructions` entry; hosts pass `ResolvedToolSet.instructions` to
86
+ * `RunConfig.instructions`, so a change is appended to the conversation instead of rewriting the
87
+ * cached request prefix.
88
+ */
89
+ readonly catalog?: 'description' | 'instructions';
81
90
  };
82
91
  type CodeModeStagingOptions = {
83
92
  /** Staged calls per script; `stage` rejects past it. At most the resolution's `maxCalls`. */readonly maxCalls?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"tool.d.mts","names":[],"sources":["../src/tool.ts"],"mappings":";;;;;;;;cA2Ea,gBAAA;AAAb;AAAA,KAGY,cAAA;kGAED,SAAA,WALkB;EAAA,SAOlB,gBAAA;EAJe;;;;EAAA,SASf,cAAA;EAAA,SAEA,cAAA,WAEA;EAAA,SAAA,SAAA;EAIa;AAAA;AAGxB;EAHwB,SAAb,aAAA;AAAA;AAAA,cAGE,qBAAA;EAAA;;;;;;;;cAUA,wBAAA;;cAGA,wBAAA;AAAA,KAED,uBAAA;EAAA,SACD,QAAA,EAAU,gBAAA,EAHR;EAAA,SAKF,IAAA;WAEA,YAAA;EAAA,SACA,MAAA,GAAS,cAAA;EANR;;;;EAAA,SAWD,QAAA,IAAY,OAAA,EAAS,OAAA;EAAA;;;;EAAA,SAKrB,SAAA,IAAa,OAAA,EAAS,OAAA,KAAY,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,SAAA;EASvD;;;;;;;EAAA,SADR,gBAAA,IAAoB,KAAA;IAAA,SAClB,IAAA,EAAM,QAAA;IAAA,SACN,OAAA,EAAS,OAAA;EAAA,MACd,MAAA,CAAO,MAAA;EA1BM;;;;;;;;;EAAA,SAoCV,eAAA,IAAmB,KAAA,EAAO,4BAAA,CAA6B,OAAA,MAAa,MAAA,CAAO,MAAA;EArB9D;;;;;;EAAA,SA4Bb,OAAA,GAAU,sBAAA;AAAA;AAAA,KAGT,sBAAA;EArBU,sGAuBX,QAAQ;AAAA;;KAIP,yBAAA;AAAA,KAEA,4BAAA;EAAA,SACD,IAAA,EAAM,QAAA;EAAA,SACN,OAAA,EAAS,yBAAA;EAAA,SACT,UAAA;EAAA,SACA,OAAA,EAAS,OAAA;EAAA,SACT,MAAA,GAAS,UAAA;AAAA;;AAhBuB;AAG3C;;;;AAEmB;AAInB;;;;AAAqC;AAErC;;;;cA+sBa,gBAAA,YACX,OAAA,EAAS,uBAAA,CAAwB,OAAA,MAChC,gBAAA,CAAiB,OAAA"}
1
+ {"version":3,"file":"tool.d.mts","names":[],"sources":["../src/tool.ts"],"mappings":";;;;;;;;cAkFa,gBAAA;AAAb;AAAA,KAGY,cAAA;kGAED,SAAA,WALkB;EAAA,SAOlB,gBAAA;EAJe;;;;EAAA,SASf,cAAA;EAAA,SAEA,cAAA,WAEA;EAAA,SAAA,SAAA;EAIa;AAAA;AAGxB;EAHwB,SAAb,aAAA;AAAA;AAAA,cAGE,qBAAA;EAAA;;;;;;;;cAUA,wBAAA;;cAGA,wBAAA;AAAA,KAED,uBAAA;EAAA,SACD,QAAA,EAAU,gBAAA,EAHR;EAAA,SAKF,IAAA;WAEA,YAAA;EAAA,SACA,MAAA,GAAS,cAAA;EANR;;;;EAAA,SAWD,QAAA,IAAY,OAAA,EAAS,OAAA;EAAA;;;;EAAA,SAKrB,SAAA,IAAa,OAAA,EAAS,OAAA,KAAY,MAAA,CAAO,MAAA,CAAO,aAAA,EAAe,SAAA;EASvD;;;;;;;EAAA,SADR,gBAAA,IAAoB,KAAA;IAAA,SAClB,IAAA,EAAM,QAAA;IAAA,SACN,OAAA,EAAS,OAAA;EAAA,MACd,MAAA,CAAO,MAAA;EA1BM;;;;;;;;;EAAA,SAoCV,eAAA,IAAmB,KAAA,EAAO,4BAAA,CAA6B,OAAA,MAAa,MAAA,CAAO,MAAA;EArB9D;;;;;;EAAA,SA4Bb,OAAA,GAAU,sBAAA;EAnBF;;;;;;;;EAAA,SA4BR,OAAA;AAAA;AAAA,KAGC,sBAAA;EAnB0E,sGAqB3E,QAAQ;AAAA;;KAIP,yBAAA;AAAA,KAEA,4BAAA;EAAA,SACD,IAAA,EAAM,QAAA;EAAA,SACN,OAAA,EAAS,yBAAA;EAAA,SACT,UAAA;EAAA,SACA,OAAA,EAAS,OAAA;EAAA,SACT,MAAA,GAAS,UAAA;AAAA;;;;AAPiB;AAErC;;;;;;;;;;;;cA0vBa,gBAAA,YACX,OAAA,EAAS,uBAAA,CAAwB,OAAA,MAChC,gBAAA,CAAiB,OAAA"}
package/dist/tool.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { codeModeCatalog, describeCodeModeTool, findCodeModeTool, renderCodeModeDescription } from "./catalog.mjs";
1
+ import { codeModeCatalog, codeModeCatalogInstruction, codeModeStageOnlyCatalog, describeCodeModeTool, findCodeModeTool, renderCodeModeDescription, renderCodeModeInstructedDescription } from "./catalog.mjs";
2
2
  import { searchCodeModeTools } from "./search.mjs";
3
3
  import { boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes } from "./output.mjs";
4
4
  import { Cause, Clock, Data, Duration, Effect, Exit, Fiber, FiberSet, Option, Predicate, Result, Scope } from "effect";
5
- import { ToolCall, ToolResult, boundNestedToolCallArgs, contentPartText, makeNestedToolCallRecorder, nestedToolCallMaxErrorChars, nestedToolCallRecordLimit, nestedToolCallResultFields, recordNestedToolCall, truncateCodePoints } from "@yolk-sdk/agent/protocol";
6
5
  import * as Schema from "effect/Schema";
6
+ import { toCodemodeIdentifier } from "@earendil-works/pi-codemode/declarations";
7
+ import { ToolCall, ToolResult, boundNestedToolCallArgs, contentPartText, makeNestedToolCallRecorder, nestedToolCallMaxErrorChars, nestedToolCallRecordLimit, nestedToolCallResultFields, recordNestedToolCall, truncateCodePoints } from "@yolk-sdk/agent/protocol";
7
8
  import { makeTool } from "@yolk-sdk/agent/tools";
8
9
  import { ToolError } from "@yolk-sdk/agent/loop";
9
10
  //#region src/tool.ts
@@ -71,7 +72,8 @@ const SearchOptions = Schema.Struct({
71
72
  namespace: Schema.optionalKey(Schema.String)
72
73
  });
73
74
  const decodeSearchOptions = Schema.decodeUnknownOption(SearchOptions);
74
- /** `searchTools`, `describeTool`, and `describeNamespace` over every nested tool. */
75
+ /** `searchTools`, `describeTool`, and `describeNamespace` over every nested tool and, while the
76
+ * script may stage, every stage-only tool. */
75
77
  const discoveryGlobals = (catalog) => [
76
78
  {
77
79
  name: "searchTools",
@@ -96,18 +98,25 @@ const discoveryGlobals = (catalog) => [
96
98
  const description = tools.find((tool) => tool.namespaceDescription !== void 0)?.namespaceDescription;
97
99
  const namespace = {
98
100
  name,
99
- tools: tools.map((tool) => ({
101
+ tools: tools.map((tool) => tool.exposure === "stage" ? {
102
+ name: tool.name,
103
+ description: tool.description,
104
+ stageOnly: true
105
+ } : {
100
106
  name: tool.identifier,
101
107
  description: tool.description
102
- }))
108
+ })
103
109
  };
104
110
  if (description !== void 0) namespace.description = description;
105
111
  return Promise.resolve(namespace);
106
112
  }
107
113
  }
108
114
  ];
109
- /** `stage(name, args)`: records one call into the script's plan (never runs it). */
110
- const stageGlobal = (plan, fork) => ({
115
+ /**
116
+ * `stage(name, args)`: records one call into the script's plan (never runs it). `name` is the raw
117
+ * tool name or its catalog identifier (what `searchTools` returns for callable tools).
118
+ */
119
+ const stageGlobal = (plan, fork, stageable) => ({
111
120
  name: "stage",
112
121
  spread: true,
113
122
  signature: "(name: string, args: object): Promise<{ staged: true; key: string; index: number }>",
@@ -116,9 +125,10 @@ const stageGlobal = (plan, fork) => ({
116
125
  const [name, params] = Array.isArray(args) ? args : [];
117
126
  if (signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("stage was cancelled."));
118
127
  if (!Predicate.isString(name)) return Promise.reject(/* @__PURE__ */ new Error("stage: the first argument must be a tool name."));
128
+ const toolName = stageable.find(({ def }) => def.name === name)?.def.name ?? stageable.find(({ def }) => toCodemodeIdentifier(def.name) === name)?.def.name ?? name;
119
129
  return new Promise((resolve, reject) => {
120
130
  fork(plan.stage({
121
- name,
131
+ name: toolName,
122
132
  params: params === void 0 ? {} : params
123
133
  }), { signal }).addObserver((exit) => {
124
134
  if (Exit.isSuccess(exit)) {
@@ -131,6 +141,20 @@ const stageGlobal = (plan, fork) => ({
131
141
  });
132
142
  }
133
143
  });
144
+ /** A direct call of a stage-only tool rejects with a hint to stage it (ADR 0007). */
145
+ const stageOnlyRejections = (stageOnly) => stageOnly.flatMap((tool) => {
146
+ const message = `${tool.name} is approval-gated: record it with stage(${JSON.stringify(tool.name)}, args) instead of calling it.`;
147
+ return tool.identifier === tool.name ? [{
148
+ key: tool.name,
149
+ message
150
+ }] : [{
151
+ key: tool.identifier,
152
+ message
153
+ }, {
154
+ key: tool.name,
155
+ message
156
+ }];
157
+ });
134
158
  const planNotice = (plan) => `\n\nStaged ${plan.count} call${plan.count === 1 ? "" : "s"} as plan ${plan.id}; nothing was applied yet. To apply them, call ${plan.reviewToolName}(${JSON.stringify({
135
159
  planId: plan.id,
136
160
  planDigest: plan.digest
@@ -235,10 +259,12 @@ const runScript = (input) => Effect.gen(function* () {
235
259
  callLabel: tool.callLabel,
236
260
  execute: callTool(tool)
237
261
  }));
262
+ const stageOnly = plan === void 0 || staging === void 0 ? [] : codeModeStageOnlyCatalog(staging.tools);
238
263
  const result = yield* Effect.tryPromise({
239
264
  try: (signal) => options.executor.execute(input.code, {
240
265
  tools,
241
- globals: plan === void 0 ? discoveryGlobals(catalog) : [...discoveryGlobals(catalog), stageGlobal(plan, stageFork)],
266
+ globals: plan === void 0 || staging === void 0 ? discoveryGlobals(catalog) : [...discoveryGlobals([...catalog, ...stageOnly]), stageGlobal(plan, stageFork, staging.tools)],
267
+ unbound: stageOnlyRejections(stageOnly),
242
268
  timeoutMs,
243
269
  memoryLimitBytes: limits.memoryLimitBytes,
244
270
  store,
@@ -421,9 +447,11 @@ const makeCodeModeTool = (options) => {
421
447
  };
422
448
  const inlineBudget = options.inlineBudget ?? 3e3;
423
449
  const store = options.loadStore !== void 0;
424
- return makeTool({
450
+ const instructed = options.catalog === "instructions";
451
+ const offered = (staging) => options.staging === false || staging === void 0 || !instructed && staging.tools.length === 0 ? void 0 : staging;
452
+ const registration = makeTool({
425
453
  name,
426
- description: renderCodeModeDescription({
454
+ description: instructed ? renderCodeModeInstructedDescription({ store }) : renderCodeModeDescription({
427
455
  tools: [],
428
456
  inlineBudget,
429
457
  store
@@ -431,16 +459,23 @@ const makeCodeModeTool = (options) => {
431
459
  parameters: CodeModeParams,
432
460
  access: "write",
433
461
  nestedToolAccess: true,
434
- describe: ({ tools, staging }) => renderCodeModeDescription(staging === void 0 || options.staging === false ? {
435
- tools,
436
- inlineBudget,
437
- store
438
- } : {
439
- tools,
440
- inlineBudget,
441
- store,
442
- staging
443
- }),
462
+ describe: ({ tools, staging }) => {
463
+ const offeredStaging = offered(staging);
464
+ if (instructed) return renderCodeModeInstructedDescription(offeredStaging === void 0 ? { store } : {
465
+ store,
466
+ staging: offeredStaging
467
+ });
468
+ return renderCodeModeDescription(offeredStaging === void 0 ? {
469
+ tools,
470
+ inlineBudget,
471
+ store
472
+ } : {
473
+ tools,
474
+ inlineBudget,
475
+ store,
476
+ staging: offeredStaging
477
+ });
478
+ },
444
479
  abandonedResult: abandonedScriptResult(limits),
445
480
  execute: ({ call, context, params, nested }) => nested === void 0 ? Effect.fail(new ToolError({
446
481
  tool: name,
@@ -455,6 +490,22 @@ const makeCodeModeTool = (options) => {
455
490
  nested
456
491
  })
457
492
  });
493
+ if (!instructed) return registration;
494
+ const instruct = ({ tools, staging }) => {
495
+ const offeredStaging = offered(staging);
496
+ return codeModeCatalogInstruction(offeredStaging === void 0 ? {
497
+ tools,
498
+ inlineBudget
499
+ } : {
500
+ tools,
501
+ inlineBudget,
502
+ staging: offeredStaging
503
+ });
504
+ };
505
+ return {
506
+ ...registration,
507
+ instruct
508
+ };
458
509
  };
459
510
  //#endregion
460
511
  export { codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool };