@yolk-sdk/codemode 0.1.0-canary.96
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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/catalog.d.mts +60 -0
- package/dist/catalog.d.mts.map +1 -0
- package/dist/catalog.mjs +143 -0
- package/dist/catalog.mjs.map +1 -0
- package/dist/classifier-tool.d.mts +78 -0
- package/dist/classifier-tool.d.mts.map +1 -0
- package/dist/classifier-tool.mjs +125 -0
- package/dist/classifier-tool.mjs.map +1 -0
- package/dist/executor.d.mts +89 -0
- package/dist/executor.d.mts.map +1 -0
- package/dist/executor.mjs +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +7 -0
- package/dist/node.d.mts +34 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +215 -0
- package/dist/node.mjs.map +1 -0
- package/dist/output.d.mts +42 -0
- package/dist/output.d.mts.map +1 -0
- package/dist/output.mjs +184 -0
- package/dist/output.mjs.map +1 -0
- package/dist/search.d.mts +23 -0
- package/dist/search.d.mts.map +1 -0
- package/dist/search.mjs +58 -0
- package/dist/search.mjs.map +1 -0
- package/dist/store.d.mts +40 -0
- package/dist/store.d.mts.map +1 -0
- package/dist/store.mjs +61 -0
- package/dist/store.mjs.map +1 -0
- package/dist/tool.d.mts +75 -0
- package/dist/tool.d.mts.map +1 -0
- package/dist/tool.mjs +282 -0
- package/dist/tool.mjs.map +1 -0
- package/package.json +63 -0
- package/src/catalog.ts +274 -0
- package/src/classifier-tool.ts +227 -0
- package/src/executor.ts +103 -0
- package/src/index.ts +76 -0
- package/src/node.ts +334 -0
- package/src/output.ts +287 -0
- package/src/search.ts +113 -0
- package/src/store.ts +93 -0
- package/src/tool.ts +547 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ToolJsonSchema } from "@yolk-sdk/agent/protocol";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
|
|
4
|
+
//#region src/executor.d.ts
|
|
5
|
+
/** A JSON Schema document. Only shapes declarations; values are never validated against it. */
|
|
6
|
+
type CodeModeJsonSchema = ToolJsonSchema;
|
|
7
|
+
/** Store contents: small JSON values by key. */
|
|
8
|
+
type CodeModeStore = Readonly<Record<string, Schema.Json>>;
|
|
9
|
+
type CodeModeToolContext = {
|
|
10
|
+
/** Aborted when the script ends (unawaited calls), times out, or the execution is aborted. */readonly signal: AbortSignal;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A function a script can call. Tools are called as `tools.<identifier>(args)` and
|
|
14
|
+
* `tools["<name>"](args)`; the identifier replaces characters that are not valid in a JavaScript
|
|
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.
|
|
17
|
+
*/
|
|
18
|
+
type CodeModeExecutorTool = {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly description?: string;
|
|
21
|
+
readonly inputSchema?: CodeModeJsonSchema;
|
|
22
|
+
readonly outputSchema?: CodeModeJsonSchema;
|
|
23
|
+
readonly execute: (args: unknown, context: CodeModeToolContext) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A top-level function (`name`) or namespace member (`namespace.member`) for scripts. Globals are
|
|
27
|
+
* host helpers: they are not tools and are not recorded as nested calls.
|
|
28
|
+
*/
|
|
29
|
+
type CodeModeExecutorGlobal = CodeModeExecutorTool & {
|
|
30
|
+
/** `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
|
+
readonly signature?: string;
|
|
32
|
+
};
|
|
33
|
+
/** One item of script output, in the order the script produced it. `data` is base64. */
|
|
34
|
+
type CodeModeOutputItem = {
|
|
35
|
+
readonly type: 'text';
|
|
36
|
+
readonly text: string;
|
|
37
|
+
} | {
|
|
38
|
+
readonly type: 'image';
|
|
39
|
+
readonly data: string;
|
|
40
|
+
readonly mimeType: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* - `script`: the script threw, failed to parse, or its TypeScript could not be stripped.
|
|
44
|
+
* - `timeout`: the deadline expired.
|
|
45
|
+
* - `aborted`: the caller's signal fired.
|
|
46
|
+
* - `sandbox`: the engine failed outside the script's control.
|
|
47
|
+
*/
|
|
48
|
+
type CodeModeErrorKind = 'script' | 'timeout' | 'aborted' | 'sandbox';
|
|
49
|
+
type CodeModeError = {
|
|
50
|
+
readonly kind: CodeModeErrorKind;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
readonly stack?: string;
|
|
53
|
+
};
|
|
54
|
+
/** Keys a successful script changed with `store()`; `delete` lists keys stored as `undefined`. */
|
|
55
|
+
type CodeModeStoreWrites = {
|
|
56
|
+
readonly set: CodeModeStore;
|
|
57
|
+
readonly delete: ReadonlyArray<string>;
|
|
58
|
+
};
|
|
59
|
+
type CodeModeExecutionResult = {
|
|
60
|
+
readonly ok: boolean; /** The script's return value (JSON); absent when it returned nothing. */
|
|
61
|
+
readonly value?: unknown;
|
|
62
|
+
readonly error?: CodeModeError; /** Kept for failed executions too, up to the failure. */
|
|
63
|
+
readonly output: ReadonlyArray<CodeModeOutputItem>; /** Only successful executions report writes. */
|
|
64
|
+
readonly storeWrites?: CodeModeStoreWrites;
|
|
65
|
+
};
|
|
66
|
+
type CodeModeExecuteOptions = {
|
|
67
|
+
readonly tools: ReadonlyArray<CodeModeExecutorTool>;
|
|
68
|
+
readonly globals: ReadonlyArray<CodeModeExecutorGlobal>; /** Overall deadline, including time spent in tools and waiting for a free execution slot. */
|
|
69
|
+
readonly timeoutMs: number; /** Heap cap of the script's VM. */
|
|
70
|
+
readonly memoryLimitBytes: number; /** Values the script reads with `load(key)`. */
|
|
71
|
+
readonly store: CodeModeStore;
|
|
72
|
+
readonly signal: AbortSignal;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Runs one script. `code` is the body of an async function (top-level `await` and `return`) and
|
|
76
|
+
* may carry TypeScript annotations: executors strip them or report a `script` error. The promise
|
|
77
|
+
* resolves for every script outcome, including failures, timeouts, and aborts; a rejection is
|
|
78
|
+
* treated as a `sandbox` failure. Running tool calls must be cancelled through their signal when
|
|
79
|
+
* the script ends.
|
|
80
|
+
*
|
|
81
|
+
* `@yolk-sdk/codemode/node` provides `makePiCodeModeExecutor`; hosts on other runtimes supply their
|
|
82
|
+
* own engine behind this interface.
|
|
83
|
+
*/
|
|
84
|
+
type CodeModeExecutor = {
|
|
85
|
+
readonly execute: (code: string, options: CodeModeExecuteOptions) => Promise<CodeModeExecutionResult>;
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
export { CodeModeError, CodeModeErrorKind, CodeModeExecuteOptions, CodeModeExecutionResult, CodeModeExecutor, CodeModeExecutorGlobal, CodeModeExecutorTool, CodeModeJsonSchema, CodeModeOutputItem, CodeModeStore, CodeModeStoreWrites, CodeModeToolContext };
|
|
89
|
+
//# sourceMappingURL=executor.d.mts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CodeModeError, CodeModeErrorKind, CodeModeExecuteOptions, CodeModeExecutionResult, CodeModeExecutor, CodeModeExecutorGlobal, CodeModeExecutorTool, CodeModeJsonSchema, CodeModeOutputItem, CodeModeStore, CodeModeStoreWrites, CodeModeToolContext } from "./executor.mjs";
|
|
2
|
+
import { CodeModeCatalogTool, CodeModeDescriptionInput, CodeModeListing, CodeModeToolExposure, codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing } from "./catalog.mjs";
|
|
3
|
+
import { ClassifierConcurrencyLimiter, ClassifierToolParams, MakeClassifierToolOptions, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool } from "./classifier-tool.mjs";
|
|
4
|
+
import { CodeModeSearchHit, CodeModeSearchOptions, codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools } from "./search.mjs";
|
|
5
|
+
import { CodeModeCallSummary, CodeModeResultSegment, boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls } from "./output.mjs";
|
|
6
|
+
import { CodeModeStructuredContent, CodeModeToolResultEntry, codeModeStoreFromToolResults, maxCodeModeStoreTotalChars, maxCodeModeStoreValueChars } from "./store.mjs";
|
|
7
|
+
import { CodeModeLimits, MakeCodeModeToolOptions, codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool } from "./tool.mjs";
|
|
8
|
+
export { type ClassifierConcurrencyLimiter, ClassifierToolParams, type CodeModeCallSummary, type CodeModeCatalogTool, type CodeModeDescriptionInput, type CodeModeError, type CodeModeErrorKind, type CodeModeExecuteOptions, type CodeModeExecutionResult, type CodeModeExecutor, type CodeModeExecutorGlobal, type CodeModeExecutorTool, type CodeModeJsonSchema, type CodeModeLimits, type CodeModeListing, type CodeModeOutputItem, type CodeModeResultSegment, type CodeModeSearchHit, type CodeModeSearchOptions, type CodeModeStore, type CodeModeStoreWrites, type CodeModeStructuredContent, type CodeModeToolContext, type CodeModeToolExposure, type CodeModeToolResultEntry, type MakeClassifierToolOptions, type MakeCodeModeToolOptions, 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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { codeModeCatalog, defaultCodeModeInlineBudget, describeCodeModeTool, estimateCodeModeTokens, findCodeModeTool, renderCodeModeDescription, selectCodeModeListing } from "./catalog.mjs";
|
|
2
|
+
import { ClassifierToolParams, classifierToolName, defaultClassifierProcessLimiter, defaultClassifierProcessMaxConcurrency, defaultClassifierToolMaxConcurrency, makeClassifierConcurrencyLimiter, makeClassifierTool } from "./classifier-tool.mjs";
|
|
3
|
+
import { codeModeSearchTokens, defaultCodeModeSearchLimit, searchCodeModeTools } from "./search.mjs";
|
|
4
|
+
import { boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls } from "./output.mjs";
|
|
5
|
+
import { codeModeDeadlineMarginMs, codeModeMinimumTimeoutMs, codeModeToolName, defaultCodeModeLimits, makeCodeModeTool } from "./tool.mjs";
|
|
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 };
|
package/dist/node.d.mts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CodeModeExecutor } from "./executor.mjs";
|
|
2
|
+
import { CodemodeWasmModule } from "@earendil-works/pi-codemode";
|
|
3
|
+
|
|
4
|
+
//#region src/node.d.ts
|
|
5
|
+
/** Default cap of concurrent script executions per executor. */
|
|
6
|
+
declare const defaultMaxConcurrentExecutions = 4;
|
|
7
|
+
type PiCodeModeExecutorOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* Compiled `quickjs-wasi/quickjs.wasm` (see pi's `loadQuickJSWasm`). Default: the file of the
|
|
10
|
+
* installed `quickjs-wasi` package, compiled once per process.
|
|
11
|
+
*/
|
|
12
|
+
readonly wasm?: CodemodeWasmModule | Promise<CodemodeWasmModule>;
|
|
13
|
+
/**
|
|
14
|
+
* Worker entry that imports `@earendil-works/pi-codemode/worker`. Default: pi's own worker file,
|
|
15
|
+
* which exists on disk when the package is not bundled (Next `serverExternalPackages`).
|
|
16
|
+
*/
|
|
17
|
+
readonly workerUrl?: string | URL; /** Concurrent executions of this executor; more wait for a free slot. Default 4. */
|
|
18
|
+
readonly maxConcurrentExecutions?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM
|
|
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
|
|
24
|
+
* executions per executor; create one executor per process (module scope) so the cap is
|
|
25
|
+
* per process.
|
|
26
|
+
*
|
|
27
|
+
* Node only. The worker inherits a copy of `process.env` (scripts cannot read it: the VM has no
|
|
28
|
+
* `process`). Under Next.js, list `@yolk-sdk/codemode`, `@earendil-works/pi-codemode`, and
|
|
29
|
+
* `quickjs-wasi` in `serverExternalPackages` so the worker file and wasm stay on disk.
|
|
30
|
+
*/
|
|
31
|
+
declare const makePiCodeModeExecutor: (options?: PiCodeModeExecutorOptions) => CodeModeExecutor;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { PiCodeModeExecutorOptions, defaultMaxConcurrentExecutions, makePiCodeModeExecutor };
|
|
34
|
+
//# sourceMappingURL=node.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/node.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import * as nodeModule from "node:module";
|
|
2
|
+
import { Option, Predicate } from "effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
import { CodemodeSandbox } from "@earendil-works/pi-codemode";
|
|
5
|
+
//#region src/node.ts
|
|
6
|
+
/** Default cap of concurrent script executions per executor. */
|
|
7
|
+
const defaultMaxConcurrentExecutions = 4;
|
|
8
|
+
/** Abort- and deadline-aware counting semaphore for executions. */
|
|
9
|
+
const makeExecutionSlots = (max) => {
|
|
10
|
+
let active = 0;
|
|
11
|
+
const waiting = [];
|
|
12
|
+
const acquire = (signal, timeoutMs) => {
|
|
13
|
+
if (signal.aborted) return Promise.resolve("aborted");
|
|
14
|
+
if (active < max) {
|
|
15
|
+
active++;
|
|
16
|
+
return Promise.resolve("acquired");
|
|
17
|
+
}
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
let timer;
|
|
20
|
+
const settle = (outcome) => {
|
|
21
|
+
const index = waiting.indexOf(settle);
|
|
22
|
+
if (index !== -1) waiting.splice(index, 1);
|
|
23
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
24
|
+
signal.removeEventListener("abort", onAbort);
|
|
25
|
+
resolve(outcome);
|
|
26
|
+
};
|
|
27
|
+
const onAbort = () => settle("aborted");
|
|
28
|
+
waiting.push(settle);
|
|
29
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
30
|
+
if (Number.isFinite(timeoutMs)) timer = setTimeout(() => settle("timeout"), Math.max(0, timeoutMs));
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const release = () => {
|
|
34
|
+
const next = waiting[0];
|
|
35
|
+
if (next === void 0) active--;
|
|
36
|
+
else next("acquired");
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
acquire,
|
|
40
|
+
release
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
const wrapperPrefix = "async function __yolkCodeMode() {\n";
|
|
44
|
+
const wrapperSuffix = "\n}";
|
|
45
|
+
const ErrorWithCode = Schema.Struct({ code: Schema.String });
|
|
46
|
+
const errorCode = (error) => Option.getOrUndefined(Option.map(Schema.decodeUnknownOption(ErrorWithCode)(error), (e) => e.code));
|
|
47
|
+
const syntaxLocation = (error) => {
|
|
48
|
+
const match = /^[^\n]*:(\d+)\n([^\n]*)\n([^\n]*)\n/.exec(error.stack ?? "");
|
|
49
|
+
if (match === null) return "";
|
|
50
|
+
const line = Number(match[1]) - 1;
|
|
51
|
+
return line >= 1 ? ` (line ${line})\n${match[2] ?? ""}\n${match[3] ?? ""}` : "";
|
|
52
|
+
};
|
|
53
|
+
const typeStripMessage = (error) => {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
if (errorCode(error) === "ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX") return `${message}. Write plain JavaScript: type annotations are stripped, but enums, namespaces, and parameter properties are not supported.`;
|
|
56
|
+
return `The script could not be parsed: ${message}${error instanceof Error ? syntaxLocation(error) : ""}`;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Strips TypeScript annotations with Node's `stripTypeScriptTypes` (Node 22.13+; whitespace
|
|
60
|
+
* replacement keeps line and column numbers). The script is wrapped in an async function so
|
|
61
|
+
* top-level `return` and `await` parse. On Node without the API, code passes through unchanged.
|
|
62
|
+
*/
|
|
63
|
+
const stripTypes = (code) => {
|
|
64
|
+
if (!Predicate.isFunction(nodeModule.stripTypeScriptTypes)) return {
|
|
65
|
+
ok: true,
|
|
66
|
+
code
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
const stripped = nodeModule.stripTypeScriptTypes(`${wrapperPrefix}${code}${wrapperSuffix}`, { mode: "strip" });
|
|
70
|
+
if (!stripped.startsWith(wrapperPrefix) || !stripped.endsWith(wrapperSuffix)) return {
|
|
71
|
+
ok: false,
|
|
72
|
+
error: {
|
|
73
|
+
kind: "script",
|
|
74
|
+
message: "The script could not be parsed."
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
code: stripped.slice(34, stripped.length - 2)
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: {
|
|
85
|
+
kind: "script",
|
|
86
|
+
message: typeStripMessage(error)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const piTool = (tool) => {
|
|
92
|
+
const converted = {
|
|
93
|
+
name: tool.name,
|
|
94
|
+
execute: (args, context) => tool.execute(args, context)
|
|
95
|
+
};
|
|
96
|
+
if (tool.description !== void 0) converted.description = tool.description;
|
|
97
|
+
if (tool.inputSchema !== void 0) converted.inputSchema = tool.inputSchema;
|
|
98
|
+
if (tool.outputSchema !== void 0) converted.outputSchema = tool.outputSchema;
|
|
99
|
+
return converted;
|
|
100
|
+
};
|
|
101
|
+
const piGlobal = (global) => {
|
|
102
|
+
const converted = piTool(global);
|
|
103
|
+
if (global.spread !== void 0) converted.spread = global.spread;
|
|
104
|
+
if (global.signature !== void 0) converted.signature = global.signature;
|
|
105
|
+
return converted;
|
|
106
|
+
};
|
|
107
|
+
const failure = (error) => ({
|
|
108
|
+
ok: false,
|
|
109
|
+
error,
|
|
110
|
+
output: []
|
|
111
|
+
});
|
|
112
|
+
const StoreWrites = Schema.Struct({
|
|
113
|
+
set: Schema.Record(Schema.String, Schema.Json),
|
|
114
|
+
delete: Schema.Array(Schema.String)
|
|
115
|
+
});
|
|
116
|
+
const decodeStoreWrites = Schema.decodeUnknownOption(StoreWrites);
|
|
117
|
+
const fromPiResult = (result) => {
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
const storeWrites = decodeStoreWrites(result.storeWrites);
|
|
120
|
+
if (Option.isNone(storeWrites)) return failure({
|
|
121
|
+
kind: "sandbox",
|
|
122
|
+
message: "The script reported store writes that are not JSON."
|
|
123
|
+
});
|
|
124
|
+
const converted = {
|
|
125
|
+
ok: true,
|
|
126
|
+
output: result.output,
|
|
127
|
+
storeWrites: storeWrites.value
|
|
128
|
+
};
|
|
129
|
+
if (result.value !== void 0) converted.value = result.value;
|
|
130
|
+
return converted;
|
|
131
|
+
}
|
|
132
|
+
const { kind, name, message, stack } = result.error;
|
|
133
|
+
const error = {
|
|
134
|
+
kind,
|
|
135
|
+
message: name !== void 0 && name.length > 0 && !message.startsWith(name) ? `${name}: ${message}` : message
|
|
136
|
+
};
|
|
137
|
+
if (stack !== void 0) error.stack = stack;
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
error,
|
|
141
|
+
output: result.output
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const describeError = (error) => error instanceof Error ? error.message : String(error);
|
|
145
|
+
/**
|
|
146
|
+
* A `CodeModeExecutor` on `@earendil-works/pi-codemode`: every execution gets a fresh QuickJS VM
|
|
147
|
+
* (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
|
|
149
|
+
* executions per executor; create one executor per process (module scope) so the cap is
|
|
150
|
+
* per process.
|
|
151
|
+
*
|
|
152
|
+
* Node only. The worker inherits a copy of `process.env` (scripts cannot read it: the VM has no
|
|
153
|
+
* `process`). Under Next.js, list `@yolk-sdk/codemode`, `@earendil-works/pi-codemode`, and
|
|
154
|
+
* `quickjs-wasi` in `serverExternalPackages` so the worker file and wasm stay on disk.
|
|
155
|
+
*/
|
|
156
|
+
const makePiCodeModeExecutor = (options = {}) => {
|
|
157
|
+
const slots = makeExecutionSlots(Math.max(1, Math.floor(options.maxConcurrentExecutions ?? 4)));
|
|
158
|
+
const run = async (code, execution, queuedAt) => {
|
|
159
|
+
const remaining = () => execution.timeoutMs - (Date.now() - queuedAt);
|
|
160
|
+
const stripped = stripTypes(code);
|
|
161
|
+
if (!stripped.ok) return failure(stripped.error);
|
|
162
|
+
const timeoutMs = remaining();
|
|
163
|
+
if (timeoutMs <= 0) return failure({
|
|
164
|
+
kind: "timeout",
|
|
165
|
+
message: `Execution timed out after ${execution.timeoutMs} ms`
|
|
166
|
+
});
|
|
167
|
+
const sandboxOptions = {
|
|
168
|
+
tools: execution.tools.map(piTool),
|
|
169
|
+
globals: execution.globals.map(piGlobal),
|
|
170
|
+
timeoutMs,
|
|
171
|
+
memoryLimitBytes: execution.memoryLimitBytes
|
|
172
|
+
};
|
|
173
|
+
if (options.wasm !== void 0) sandboxOptions.wasm = options.wasm;
|
|
174
|
+
if (options.workerUrl !== void 0) sandboxOptions.workerUrl = options.workerUrl;
|
|
175
|
+
let sandbox;
|
|
176
|
+
try {
|
|
177
|
+
sandbox = new CodemodeSandbox(sandboxOptions);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return failure({
|
|
180
|
+
kind: "sandbox",
|
|
181
|
+
message: `Could not create the sandbox: ${describeError(error)}`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
return fromPiResult(await sandbox.execute(stripped.code, {
|
|
186
|
+
signal: execution.signal,
|
|
187
|
+
store: execution.store,
|
|
188
|
+
timeoutMs
|
|
189
|
+
}));
|
|
190
|
+
} finally {
|
|
191
|
+
await sandbox.close();
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
return { execute: async (code, execution) => {
|
|
195
|
+
const queuedAt = Date.now();
|
|
196
|
+
const slot = await slots.acquire(execution.signal, execution.timeoutMs);
|
|
197
|
+
if (slot === "aborted") return failure({
|
|
198
|
+
kind: "aborted",
|
|
199
|
+
message: "Execution aborted"
|
|
200
|
+
});
|
|
201
|
+
if (slot === "timeout") return failure({
|
|
202
|
+
kind: "timeout",
|
|
203
|
+
message: `Execution timed out after ${execution.timeoutMs} ms while waiting for a free execution slot`
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
return await run(code, execution, queuedAt);
|
|
207
|
+
} finally {
|
|
208
|
+
slots.release();
|
|
209
|
+
}
|
|
210
|
+
} };
|
|
211
|
+
};
|
|
212
|
+
//#endregion
|
|
213
|
+
export { defaultMaxConcurrentExecutions, makePiCodeModeExecutor };
|
|
214
|
+
|
|
215
|
+
//# sourceMappingURL=node.mjs.map
|
|
@@ -0,0 +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 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\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): 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 = stack\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, 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(stripped.code, {\n signal: execution.signal,\n store: execution.store,\n timeoutMs\n })\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":";;;;;;AAqBA,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;AAEtB,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,WAAoD;CACxE,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;CAEvC,OAAO;EAAE,IAAI;EAAO;EAAO,QAAQ,OAAO;CAAO;AACnD;AAEA,MAAM,iBAAiB,UAAoB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;;;;;;;;;;;;AAahG,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,SAAS,MAAM;IACnC,QAAQ,UAAU;IAClB,OAAO,UAAU;IACjB;GACF,CAAC,CACH;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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CodeModeExecutionResult, CodeModeOutputItem } from "./executor.mjs";
|
|
2
|
+
import { Content, NestedToolCallStatus } from "@yolk-sdk/agent/protocol";
|
|
3
|
+
|
|
4
|
+
//#region src/output.d.ts
|
|
5
|
+
/** Text or image piece of the model-visible result, in order. */
|
|
6
|
+
type CodeModeResultSegment = CodeModeOutputItem;
|
|
7
|
+
/**
|
|
8
|
+
* Keeps at most about `maxChars` characters of text: the head and the tail of the segments, with an
|
|
9
|
+
* omission marker in between. Images inside the omitted middle are dropped and counted.
|
|
10
|
+
*/
|
|
11
|
+
declare const boundCodeModeSegments: (segments: ReadonlyArray<CodeModeResultSegment>, maxChars: number) => ReadonlyArray<CodeModeResultSegment>;
|
|
12
|
+
/** Agent `Content`: a plain string without images, ordered text and image parts otherwise. */
|
|
13
|
+
declare const codeModeSegmentsContent: (segments: ReadonlyArray<CodeModeResultSegment>) => Content;
|
|
14
|
+
type CodeModeCallSummary = {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly status: NestedToolCallStatus;
|
|
17
|
+
};
|
|
18
|
+
/** `name: 2 ok, 1 error; other: 1 cancelled`, by first appearance. */
|
|
19
|
+
declare const summarizeCodeModeCalls: (calls: ReadonlyArray<CodeModeCallSummary>) => string;
|
|
20
|
+
/** Default cap of images kept in one result. */
|
|
21
|
+
declare const defaultCodeModeMaxImages = 8;
|
|
22
|
+
/** Default cap of base64 image data kept in one result, in characters (4 MiB). */
|
|
23
|
+
declare const defaultCodeModeMaxImageBytes: number;
|
|
24
|
+
/**
|
|
25
|
+
* Model-visible segments of one execution: a `Script completed` / `Script failed` header with the
|
|
26
|
+
* wall time, the output in order, the JSON return value (omitted when undefined), and for failures
|
|
27
|
+
* the error and the tool calls already made. Images are kept in order while they fit `maxImages`
|
|
28
|
+
* (default 8) and `maxImageBytes` of base64 in total (default 4 MiB); an image that would exceed
|
|
29
|
+
* either is dropped and counted in an omission note after the output. The result is then bounded
|
|
30
|
+
* by `maxChars` with a head-and-tail cut, which also drops images in the omitted middle.
|
|
31
|
+
*/
|
|
32
|
+
declare const codeModeResultSegments: (input: {
|
|
33
|
+
readonly result: CodeModeExecutionResult;
|
|
34
|
+
readonly wallTimeMs: number;
|
|
35
|
+
readonly calls: ReadonlyArray<CodeModeCallSummary>;
|
|
36
|
+
readonly maxChars: number;
|
|
37
|
+
readonly maxImages?: number;
|
|
38
|
+
readonly maxImageBytes?: number;
|
|
39
|
+
}) => ReadonlyArray<CodeModeResultSegment>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { CodeModeCallSummary, CodeModeResultSegment, boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls };
|
|
42
|
+
//# sourceMappingURL=output.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.mts","names":[],"sources":["../src/output.ts"],"mappings":";;;;;KAUY,qBAAA,GAAwB,kBAAkB;AAAtD;;;;AAAA,cA8Ba,qBAAA,GACX,QAAA,EAAU,aAAA,CAAc,qBAAA,GACxB,QAAA,aACC,aAAA,CAAc,qBAAA;AAHjB;AAAA,cA6Fa,uBAAA,GACX,QAAA,EAAU,aAAA,CAAc,qBAAA,MACvB,OAAA;AAAA,KAiBS,mBAAA;EAAA,SACD,IAAA;EAAA,SACA,MAAA,EAAQ,oBAAoB;AAAA;;cAM1B,sBAAA,GAA0B,KAAA,EAAO,aAAa,CAAC,mBAAA;;cAwC/C,wBAAA;;cAGA,4BAAA;;;;;AAhKyB;AA0FtC;;;cAmFa,sBAAA,GAA0B,KAAA;EAAA,SAC5B,MAAA,EAAQ,uBAAA;EAAA,SACR,UAAA;EAAA,SACA,KAAA,EAAO,aAAA,CAAc,mBAAA;EAAA,SACrB,QAAA;EAAA,SACA,SAAA;EAAA,SACA,aAAA;AAAA,MACP,aAAA,CAAc,qBAAA"}
|
package/dist/output.mjs
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ImagePart, TextPart, inlineBase64AttachmentSource } from "@yolk-sdk/agent/protocol";
|
|
2
|
+
//#region src/output.ts
|
|
3
|
+
const isHighSurrogate = (code) => code >= 55296 && code <= 56319;
|
|
4
|
+
const isLowSurrogate = (code) => code >= 56320 && code <= 57343;
|
|
5
|
+
const headCut = (text, length) => length > 0 && length < text.length && isHighSurrogate(text.charCodeAt(length - 1)) ? length - 1 : length;
|
|
6
|
+
const tailCut = (text, start) => start > 0 && start < text.length && isLowSurrogate(text.charCodeAt(start)) ? start + 1 : start;
|
|
7
|
+
const textLength = (segments) => segments.reduce((total, segment) => total + (segment.type === "text" ? segment.text.length : 0), 0);
|
|
8
|
+
const omissionMarker = (characters, images) => `\n\n[… ${characters} characters${images > 0 ? ` and ${images} image${images === 1 ? "" : "s"}` : ""} omitted …]\n\n`;
|
|
9
|
+
const markerReserve = 80;
|
|
10
|
+
/**
|
|
11
|
+
* Keeps at most about `maxChars` characters of text: the head and the tail of the segments, with an
|
|
12
|
+
* omission marker in between. Images inside the omitted middle are dropped and counted.
|
|
13
|
+
*/
|
|
14
|
+
const boundCodeModeSegments = (segments, maxChars) => {
|
|
15
|
+
const total = textLength(segments);
|
|
16
|
+
if (total <= maxChars) return segments;
|
|
17
|
+
const budget = Math.max(0, maxChars - markerReserve);
|
|
18
|
+
let headBudget = Math.ceil(budget / 2);
|
|
19
|
+
let tailBudget = budget - headBudget;
|
|
20
|
+
const head = [];
|
|
21
|
+
let headIndex = 0;
|
|
22
|
+
let headPartial = 0;
|
|
23
|
+
for (; headIndex < segments.length && headBudget > 0; headIndex++) {
|
|
24
|
+
const segment = segments[headIndex];
|
|
25
|
+
if (segment === void 0) break;
|
|
26
|
+
if (segment.type === "image") {
|
|
27
|
+
head.push(segment);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (segment.text.length <= headBudget) {
|
|
31
|
+
head.push(segment);
|
|
32
|
+
headBudget -= segment.text.length;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
headPartial = headCut(segment.text, headBudget);
|
|
36
|
+
head.push({
|
|
37
|
+
type: "text",
|
|
38
|
+
text: segment.text.slice(0, headPartial)
|
|
39
|
+
});
|
|
40
|
+
headBudget = 0;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const tail = [];
|
|
44
|
+
let tailIndex = segments.length - 1;
|
|
45
|
+
let tailStart;
|
|
46
|
+
for (; tailIndex >= headIndex && tailBudget > 0; tailIndex--) {
|
|
47
|
+
const segment = segments[tailIndex];
|
|
48
|
+
if (segment === void 0) break;
|
|
49
|
+
if (segment.type === "image") {
|
|
50
|
+
if (tailIndex === headIndex) break;
|
|
51
|
+
tail.unshift(segment);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const available = tailIndex === headIndex ? segment.text.length - headPartial : segment.text.length;
|
|
55
|
+
if (available <= tailBudget && tailIndex !== headIndex) {
|
|
56
|
+
tail.unshift(segment);
|
|
57
|
+
tailBudget -= available;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
tailStart = tailCut(segment.text, segment.text.length - Math.min(available, tailBudget));
|
|
61
|
+
tail.unshift({
|
|
62
|
+
type: "text",
|
|
63
|
+
text: segment.text.slice(tailStart)
|
|
64
|
+
});
|
|
65
|
+
tailBudget = 0;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const keptText = textLength(head) + textLength(tail);
|
|
69
|
+
const keptImages = [...head, ...tail].filter((segment) => segment.type === "image").length;
|
|
70
|
+
const allImages = segments.filter((segment) => segment.type === "image").length;
|
|
71
|
+
return [
|
|
72
|
+
...head,
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: omissionMarker(total - keptText, allImages - keptImages)
|
|
76
|
+
},
|
|
77
|
+
...tail
|
|
78
|
+
];
|
|
79
|
+
};
|
|
80
|
+
const mergeText = (segments) => segments.reduce((merged, segment) => {
|
|
81
|
+
const last = merged.at(-1);
|
|
82
|
+
if (segment.type === "text" && last?.type === "text") merged[merged.length - 1] = {
|
|
83
|
+
type: "text",
|
|
84
|
+
text: last.text + segment.text
|
|
85
|
+
};
|
|
86
|
+
else if (segment.type === "image" || segment.text.length > 0) merged.push(segment);
|
|
87
|
+
return merged;
|
|
88
|
+
}, []);
|
|
89
|
+
/** Agent `Content`: a plain string without images, ordered text and image parts otherwise. */
|
|
90
|
+
const codeModeSegmentsContent = (segments) => {
|
|
91
|
+
const merged = mergeText(segments);
|
|
92
|
+
if (merged.every((segment) => segment.type === "text")) return merged.map((segment) => segment.type === "text" ? segment.text : "").join("");
|
|
93
|
+
return merged.map((segment) => segment.type === "text" ? TextPart.make({ text: segment.text }) : ImagePart.make({
|
|
94
|
+
source: inlineBase64AttachmentSource(segment.data),
|
|
95
|
+
mimeType: segment.mimeType
|
|
96
|
+
}));
|
|
97
|
+
};
|
|
98
|
+
const statusOrder = [
|
|
99
|
+
"ok",
|
|
100
|
+
"error",
|
|
101
|
+
"cancelled"
|
|
102
|
+
];
|
|
103
|
+
/** `name: 2 ok, 1 error; other: 1 cancelled`, by first appearance. */
|
|
104
|
+
const summarizeCodeModeCalls = (calls) => {
|
|
105
|
+
if (calls.length === 0) return "none";
|
|
106
|
+
const byName = /* @__PURE__ */ new Map();
|
|
107
|
+
for (const call of calls) {
|
|
108
|
+
const counts = byName.get(call.name) ?? /* @__PURE__ */ new Map();
|
|
109
|
+
counts.set(call.status, (counts.get(call.status) ?? 0) + 1);
|
|
110
|
+
byName.set(call.name, counts);
|
|
111
|
+
}
|
|
112
|
+
return [...byName].map(([name, counts]) => `${name}: ${statusOrder.flatMap((status) => {
|
|
113
|
+
const count = counts.get(status);
|
|
114
|
+
return count === void 0 ? [] : [`${count} ${status}`];
|
|
115
|
+
}).join(", ")}`).join("; ");
|
|
116
|
+
};
|
|
117
|
+
const stackFrames = (stack) => stack === void 0 ? [] : stack.split("\n").filter((line) => /^\s+at\s/.test(line) || /^\s*\S+:\d+/.test(line));
|
|
118
|
+
const encodeValue = (value) => {
|
|
119
|
+
try {
|
|
120
|
+
return JSON.stringify(value) ?? "undefined";
|
|
121
|
+
} catch {
|
|
122
|
+
return "[unserializable value]";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** Default cap of images kept in one result. */
|
|
126
|
+
const defaultCodeModeMaxImages = 8;
|
|
127
|
+
/** Default cap of base64 image data kept in one result, in characters (4 MiB). */
|
|
128
|
+
const defaultCodeModeMaxImageBytes = 4 * 1024 * 1024;
|
|
129
|
+
const imageOmissionNote = (dropped, maxImages, maxImageBytes) => `\n\n[… ${dropped} image${dropped === 1 ? "" : "s"} omitted: a result keeps at most ${maxImages} images and ${maxImageBytes} characters of base64 image data …]`;
|
|
130
|
+
/**
|
|
131
|
+
* Model-visible segments of one execution: a `Script completed` / `Script failed` header with the
|
|
132
|
+
* wall time, the output in order, the JSON return value (omitted when undefined), and for failures
|
|
133
|
+
* the error and the tool calls already made. Images are kept in order while they fit `maxImages`
|
|
134
|
+
* (default 8) and `maxImageBytes` of base64 in total (default 4 MiB); an image that would exceed
|
|
135
|
+
* either is dropped and counted in an omission note after the output. The result is then bounded
|
|
136
|
+
* by `maxChars` with a head-and-tail cut, which also drops images in the omitted middle.
|
|
137
|
+
*/
|
|
138
|
+
const codeModeResultSegments = (input) => {
|
|
139
|
+
const { result } = input;
|
|
140
|
+
const maxImages = Math.max(0, Math.floor(input.maxImages ?? 8));
|
|
141
|
+
const maxImageBytes = Math.max(0, Math.floor(input.maxImageBytes ?? 4194304));
|
|
142
|
+
const ms = Math.round(input.wallTimeMs);
|
|
143
|
+
const segments = [];
|
|
144
|
+
const text = (value) => {
|
|
145
|
+
segments.push({
|
|
146
|
+
type: "text",
|
|
147
|
+
text: value
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
text(result.ok ? `Script completed in ${ms} ms.` : `Script failed after ${ms} ms.`);
|
|
151
|
+
if (result.output.length > 0) {
|
|
152
|
+
text("\n\nOutput:\n");
|
|
153
|
+
let keptImages = 0;
|
|
154
|
+
let keptImageBytes = 0;
|
|
155
|
+
let droppedImages = 0;
|
|
156
|
+
result.output.forEach((item, index) => {
|
|
157
|
+
const previous = result.output[index - 1];
|
|
158
|
+
if (item.type === "text") text(previous?.type === "text" ? `\n${item.text}` : item.text);
|
|
159
|
+
else if (keptImages < maxImages && keptImageBytes + item.data.length <= maxImageBytes) {
|
|
160
|
+
keptImages++;
|
|
161
|
+
keptImageBytes += item.data.length;
|
|
162
|
+
segments.push(item);
|
|
163
|
+
} else droppedImages++;
|
|
164
|
+
});
|
|
165
|
+
if (droppedImages > 0) text(imageOmissionNote(droppedImages, maxImages, maxImageBytes));
|
|
166
|
+
}
|
|
167
|
+
if (result.ok && result.value !== void 0) text(`\n\nReturn value:\n${encodeValue(result.value)}`);
|
|
168
|
+
if (!result.ok) {
|
|
169
|
+
const error = result.error ?? {
|
|
170
|
+
kind: "sandbox",
|
|
171
|
+
message: "The script failed without an error."
|
|
172
|
+
};
|
|
173
|
+
text(`\n\nScript error: ${error.kind}: ${error.message}`);
|
|
174
|
+
const frames = stackFrames(error.stack);
|
|
175
|
+
if (frames.length > 0) text(`\n${frames.join("\n")}`);
|
|
176
|
+
text(`\n\nTool calls made before the failure (they are not undone): ${summarizeCodeModeCalls(input.calls)}.`);
|
|
177
|
+
}
|
|
178
|
+
if (result.ok && result.output.length === 0 && result.value === void 0) text(" No output and no return value.");
|
|
179
|
+
return boundCodeModeSegments(mergeText(segments), input.maxChars);
|
|
180
|
+
};
|
|
181
|
+
//#endregion
|
|
182
|
+
export { boundCodeModeSegments, codeModeResultSegments, codeModeSegmentsContent, defaultCodeModeMaxImageBytes, defaultCodeModeMaxImages, summarizeCodeModeCalls };
|
|
183
|
+
|
|
184
|
+
//# sourceMappingURL=output.mjs.map
|