blob: da42bce7035be50470b579fa24d5987dbb8597fb [file]
export interface CodeModeToolDefinition {
name: string;
}
/**
* The byte definition shared by the interpreter and Runtime's nested-result
* publication boundary. JSON is the representation that is persisted and
* returned to the cell, including quotes and escapes for strings.
*/
export function serializedByteLength(value: unknown, maxBytes = Number.POSITIVE_INFINITY): number {
const limit =
Number.isFinite(maxBytes) && maxBytes >= 0 ? Math.floor(maxBytes) : Number.POSITIVE_INFINITY;
const budget: SerializedByteBudget = { bytes: 0, limit, seen: new Set() };
try {
if (!countSerializedValue(value, budget, 'top')) return Number.POSITIVE_INFINITY;
} catch {
return Number.POSITIVE_INFINITY;
}
return budget.bytes;
}
interface SerializedByteBudget {
bytes: number;
limit: number;
seen: Set<object>;
}
function countSerializedValue(
value: unknown,
budget: SerializedByteBudget,
position: 'top' | 'array' | 'object',
): boolean {
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
if (position === 'object') return false;
if (position === 'top' && value !== undefined) return false;
addSerializedBytes(budget, 4);
return true;
}
if (value === null) {
addSerializedBytes(budget, 4);
return true;
}
if (typeof value === 'string') {
countJsonString(value, budget);
return true;
}
if (typeof value === 'boolean') {
addSerializedBytes(budget, value ? 4 : 5);
return true;
}
if (typeof value === 'number') {
const encoded = Number.isFinite(value) ? JSON.stringify(value) : 'null';
addSerializedBytes(budget, encoded.length);
return true;
}
if (typeof value === 'bigint' || typeof value !== 'object') return false;
if (budget.seen.has(value)) return false;
if (typeof (value as { toJSON?: unknown }).toJSON === 'function') return false;
if (!Array.isArray(value)) {
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) return false;
}
budget.seen.add(value);
try {
if (Array.isArray(value)) {
if (!addSerializedBytes(budget, 1)) return true;
for (let index = 0; index < value.length; index += 1) {
if (index > 0 && !addSerializedBytes(budget, 1)) return true;
if (!countSerializedValue(value[index], budget, 'array')) return false;
if (budget.bytes > budget.limit) return true;
}
addSerializedBytes(budget, 1);
return true;
}
if (!addSerializedBytes(budget, 1)) return true;
let emitted = 0;
for (const key of Object.keys(value)) {
const item = (value as Record<string, unknown>)[key];
if (item === undefined || typeof item === 'function' || typeof item === 'symbol') continue;
if (emitted > 0 && !addSerializedBytes(budget, 1)) return true;
if (!countJsonString(key, budget)) return true;
if (!addSerializedBytes(budget, 1)) return true;
if (!countSerializedValue(item, budget, 'object')) return false;
emitted += 1;
if (budget.bytes > budget.limit) return true;
}
addSerializedBytes(budget, 1);
return true;
} finally {
budget.seen.delete(value);
}
}
function countJsonString(value: string, budget: SerializedByteBudget): boolean {
if (!addSerializedBytes(budget, 1)) return false;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
let bytes: number;
if (
code === 0x22 ||
code === 0x5c ||
code === 0x08 ||
code === 0x09 ||
code === 0x0a ||
code === 0x0c ||
code === 0x0d
) {
bytes = 2;
} else if (code <= 0x1f) {
bytes = 6;
} else if (code >= 0xd800 && code <= 0xdbff) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes = 4;
index += 1;
} else {
bytes = 6;
}
} else if (code >= 0xdc00 && code <= 0xdfff) {
bytes = 6;
} else if (code <= 0x7f) {
bytes = 1;
} else if (code <= 0x7ff) {
bytes = 2;
} else {
bytes = 3;
}
if (!addSerializedBytes(budget, bytes)) return false;
}
return addSerializedBytes(budget, 1);
}
function addSerializedBytes(budget: SerializedByteBudget, bytes: number): boolean {
if (budget.bytes > Number.MAX_SAFE_INTEGER - bytes) {
budget.bytes = Number.POSITIVE_INFINITY;
return false;
}
if (budget.bytes + bytes > budget.limit) {
budget.bytes =
budget.limit < Number.MAX_SAFE_INTEGER ? budget.limit + 1 : Number.POSITIVE_INFINITY;
return false;
}
budget.bytes += bytes;
return true;
}
export interface CodeModeLimits {
maxSourceBytes: number;
maxSteps: number;
maxWallTimeMs: number;
maxToolCalls: number;
maxConcurrency: number;
maxIntermediateBytes: number;
maxCollectionItems: number;
maxResultBytes: number;
maxOutputBytes: number;
maxDataDepth: number;
}
export const DEFAULT_CODE_MODE_LIMITS: Readonly<CodeModeLimits> = Object.freeze({
maxSourceBytes: 64 * 1024,
maxSteps: 100_000,
maxWallTimeMs: 30_000,
maxToolCalls: 32,
maxConcurrency: 8,
maxIntermediateBytes: 1024 * 1024,
maxCollectionItems: 100_000,
maxResultBytes: 1024 * 1024,
maxOutputBytes: 1024 * 1024,
maxDataDepth: 32,
});
export type CodeModeDiagnosticKind =
| 'parse_error'
| 'unsupported_syntax'
| 'execution_error'
| 'unknown_tool'
| 'invalid_data'
| 'limit_exceeded'
| 'tool_failure';
export interface CodeModeDiagnostic {
kind: CodeModeDiagnosticKind;
message: string;
location?: { line: number; column: number };
}
export interface CodeModeToolCall {
index: number;
name: string;
}
export interface CodeModeExecutionSuccess {
ok: true;
value: unknown;
toolCalls: CodeModeToolCall[];
}
export interface CodeModeExecutionFailure {
ok: false;
error: CodeModeDiagnostic;
toolCalls: CodeModeToolCall[];
}
export type CodeModeExecutionResult = CodeModeExecutionSuccess | CodeModeExecutionFailure;
export interface ExecuteCodeCellInput {
code: string;
tools: readonly CodeModeToolDefinition[];
callTool(name: string, input: unknown, signal: AbortSignal): Promise<unknown>;
isFatalToolError?: (error: unknown) => boolean;
signal?: AbortSignal;
limits?: Partial<CodeModeLimits>;
}
export async function executeCodeCell(
input: ExecuteCodeCellInput,
): Promise<CodeModeExecutionResult> {
return executeCodeCellImpl(input);
}
import { executeCodeCellImpl } from './interpreter.js';