@code-yeongyu/senpi-codemode 2026.10.8 → 2026.10.10-10
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/CHANGELOG.md +178 -0
- package/README.md +212 -14
- package/package.json +6 -5
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/protocol.ts +12 -3
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +48 -0
- package/src/bridges/eval-virtual-schemas.ts +12 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +68 -4
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +38 -7
- package/src/extension/bridge-tool-call.ts +7 -1
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +53 -3
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +26 -4
- package/src/index.ts +4 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +48 -39
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +21 -5
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +45 -9
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +5 -0
- package/src/kernels/js/worker-slot.ts +9 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/kernel.ts +8 -2
- package/src/kernels/py/prelude.py +145 -11
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/py/transport.ts +13 -1
- package/src/kernels/rb/workpool.rb +7 -1
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/cell-source-at-start.ts +18 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/subprocess-process.ts +14 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +13 -12
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +6 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +88 -0
- package/src/tool/magic-cell-host.ts +123 -0
- package/src/tool/magic-cells.ts +75 -0
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +73 -1
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +50 -1
|
@@ -151,6 +151,11 @@ export class SubprocessProcess {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
private kill(signal: NodeJS.Signals): void {
|
|
154
|
+
if (globalThis.process.platform === "win32") {
|
|
155
|
+
if (this.child.pid !== undefined) killProcessTree(this.child.pid);
|
|
156
|
+
else this.child.kill(signal);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
154
159
|
if (this.child.pid !== undefined) {
|
|
155
160
|
try {
|
|
156
161
|
globalThis.process.kill(-this.child.pid, signal);
|
|
@@ -163,6 +168,15 @@ export class SubprocessProcess {
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
// Windows has no process-group signal: taskkill /T /F walks the tree rooted at the child.
|
|
172
|
+
function killProcessTree(pid: number): void {
|
|
173
|
+
try {
|
|
174
|
+
nodeSpawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!(error instanceof Error)) throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
export function spawnSubprocess(spawn: SubprocessSpawn | undefined, request: SubprocessSpawnRequest): SubprocessLike {
|
|
167
181
|
if (spawn) return spawn(request.command, request.args, { cwd: request.cwd, env: request.env });
|
|
168
182
|
const child = nodeSpawn(request.command, [...request.args], {
|
|
@@ -18,6 +18,8 @@ export interface EvalPromptParts {
|
|
|
18
18
|
|
|
19
19
|
export interface EvalPromptOptions {
|
|
20
20
|
readonly spawns: boolean;
|
|
21
|
+
/** `prompt.advertiseHelpers`: true appends one pointer line to the helper documentation entry. */
|
|
22
|
+
readonly advertiseHelpers?: boolean;
|
|
21
23
|
/** Whether the session registry exposes the monitor tool through eval. */
|
|
22
24
|
readonly monitor?: boolean;
|
|
23
25
|
readonly spawnDefaultAgent?: string;
|
|
@@ -74,6 +76,10 @@ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyl
|
|
|
74
76
|
type ContextValue = string | boolean;
|
|
75
77
|
type Context = Readonly<Record<string, ContextValue>>;
|
|
76
78
|
|
|
79
|
+
/** The single line `prompt.advertiseHelpers: true` adds; off by default, so the default description is unchanged. */
|
|
80
|
+
export const ADVERTISED_HELPERS_LINE =
|
|
81
|
+
"Advanced cell helpers (handle controls, wait(), kernel tools in workpools) are documented on demand: tool_schema('eval:helpers').";
|
|
82
|
+
|
|
77
83
|
export function buildEvalPrompt(
|
|
78
84
|
enabled: EnabledLanguages,
|
|
79
85
|
options: EvalPromptOptions = { spawns: false },
|
|
@@ -104,9 +110,10 @@ export function buildEvalPrompt(
|
|
|
104
110
|
maxDetachedCells: String(options.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS),
|
|
105
111
|
kernelPreludeDocs: (options.kernelPreludes ?? []).map((prelude) => prelude.documentation).join("\n"),
|
|
106
112
|
};
|
|
107
|
-
const
|
|
113
|
+
const rendered = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
108
114
|
.replace(/\n{3,}/g, "\n\n")
|
|
109
115
|
.trim();
|
|
116
|
+
const description = options.advertiseHelpers === true ? `${rendered}\n\n${ADVERTISED_HELPERS_LINE}` : rendered;
|
|
110
117
|
return {
|
|
111
118
|
description,
|
|
112
119
|
promptSnippet: "Run one incremental code cell in a persistent language kernel.",
|
package/src/tool/AGENTS.md
CHANGED
|
@@ -17,6 +17,7 @@ anchor most suites).
|
|
|
17
17
|
| Code preview layout | `code-preview.ts` -> `display-code.ts` (JS: `display-js*.ts` Bun-only masked `Bun.Transpiler`; Python: `display-python*.ts` user interpreter, background + repaint) |
|
|
18
18
|
| Status events, execution events | `status-events.ts`, `eval-execution-event.ts` |
|
|
19
19
|
| Interrupt, capture | `interrupt-note.ts`, `call-capture.ts` |
|
|
20
|
+
| Magic cells | `magic-cells.ts` (parsing), `magic-cell-host.ts` (`%pip`, `%bun`/`%npm`, `%environment`), `load-cell.ts` (`%load`); installs live in `../environments/` |
|
|
20
21
|
|
|
21
22
|
## CONVENTIONS
|
|
22
23
|
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
14
14
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
15
15
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
16
16
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
17
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
17
18
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
18
19
|
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
19
20
|
import {
|
|
@@ -48,12 +49,14 @@ export interface CellBridgeRuntime {
|
|
|
48
49
|
readonly ctx: ExtensionContext;
|
|
49
50
|
readonly artifactPath?: string;
|
|
50
51
|
readonly imageResizer?: EvalImageResizer;
|
|
51
|
-
/** This cell's live kernel-tool capability;
|
|
52
|
+
/** This cell's live kernel-tool capability; JS and Python kernels have one (#1754, #2731). */
|
|
52
53
|
readonly kernelTools?: KernelToolsCapability;
|
|
53
54
|
/** The session generation's handle registry behind `wait()` / `handle()` / completion handles. */
|
|
54
55
|
readonly handles?: HandleRegistry;
|
|
55
56
|
/** Absolute wall-clock deadline of this cell; a completion handle it creates is bounded by it. */
|
|
56
57
|
readonly hardDeadlineMs?: number;
|
|
58
|
+
/** A JS cell's session environment for `packages.install()`; Python cells reach theirs over the bridge. */
|
|
59
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export class CellHandler {
|
|
@@ -235,22 +238,20 @@ export class CellHandler {
|
|
|
235
238
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
236
239
|
} catch (error) {
|
|
237
240
|
if (!this.#state.active) return;
|
|
238
|
-
const
|
|
239
|
-
error instanceof Error ? error.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
const code =
|
|
242
|
+
error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
+
// A blocked call (a permission denial or another hook's veto) is not an argument problem.
|
|
245
|
+
const text =
|
|
246
|
+
code === "blocked"
|
|
247
|
+
? errorMessage
|
|
248
|
+
: appendSchemaHint(errorMessage, message.toolName, this.#toolParameters(message.toolName));
|
|
243
249
|
recordToolCall(this.#state.toolCalls, false, capture, undefined, text);
|
|
244
250
|
this.#kernel.deliverToolReply({
|
|
245
251
|
type: "tool-reply",
|
|
246
252
|
callId: message.callId,
|
|
247
253
|
ok: false,
|
|
248
|
-
error: {
|
|
249
|
-
message: text,
|
|
250
|
-
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
251
|
-
? { code: error.code }
|
|
252
|
-
: {}),
|
|
253
|
-
},
|
|
254
|
+
error: { message: text, ...(code === undefined ? {} : { code }) },
|
|
254
255
|
});
|
|
255
256
|
}
|
|
256
257
|
this.#resultBuilder.emitUpdate(false);
|
|
@@ -29,6 +29,7 @@ export function reservedDispatchContext(
|
|
|
29
29
|
marshalToolResult,
|
|
30
30
|
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
31
31
|
...(host === undefined ? {} : { evalHandleHost: host }),
|
|
32
|
+
...(runtime.environments === undefined ? {} : { environments: runtime.environments }),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
package/src/tool/cell-runtime.ts
CHANGED
|
@@ -219,8 +219,10 @@ export class CellResultBuilder {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
#liveUpdateText(): string {
|
|
222
|
-
if (this.#state.status === "queued" && this.#state.queuedBehind
|
|
223
|
-
return
|
|
222
|
+
if (this.#state.status === "queued" && this.#state.queuedBehind !== undefined) {
|
|
223
|
+
return this.#state.queuedBehind.length === 0
|
|
224
|
+
? `waiting for the ${this.#state.input.language} kernel to be ready`
|
|
225
|
+
: `queued behind ${this.#state.queuedBehind.join(", ")} in the ${this.#state.input.language} kernel`;
|
|
224
226
|
}
|
|
225
227
|
const summary = this.#state.input.summary === undefined ? "" : ` ${this.#state.input.summary}`;
|
|
226
228
|
const output = this.#liveLines.text();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { WakeSourceState } from "../extension/wake-source-state.ts";
|
|
3
|
+
import type { EvalImageContent } from "./image-resize.ts";
|
|
3
4
|
import type { EvalLanguage, EvalToolDetails } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
export type EvalDetachedCellState = "queued" | "running" | "detached" | "completed" | "failed" | "cancelled";
|
|
@@ -24,6 +25,8 @@ export interface EvalDetachedCellSnapshot {
|
|
|
24
25
|
export interface EvalDetachedCellNotification {
|
|
25
26
|
readonly cellId: string;
|
|
26
27
|
readonly content: string;
|
|
28
|
+
/** Images the cell displayed, delivered with the notification the way a foreground result delivers them. */
|
|
29
|
+
readonly images?: readonly EvalImageContent[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface EvalDetachedCellNotifier {
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
EvalDetachedCellSnapshot,
|
|
14
14
|
EvalDetachedCellStatusEntry,
|
|
15
15
|
} from "./detached-cell-contract.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { cancelledDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
|
|
17
17
|
import {
|
|
18
18
|
activeDetachedCellReuseError,
|
|
19
19
|
allowsDetachedCellTransition,
|
|
@@ -36,7 +36,6 @@ export type {
|
|
|
36
36
|
} from "./detached-cell-contract.ts";
|
|
37
37
|
|
|
38
38
|
export class EvalDetachedCellManager {
|
|
39
|
-
readonly #artifactsDir: string | undefined;
|
|
40
39
|
readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
|
|
41
40
|
readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
|
|
42
41
|
readonly #cells = new Map<string, ManagedCell>();
|
|
@@ -49,7 +48,6 @@ export class EvalDetachedCellManager {
|
|
|
49
48
|
readonly #maxDetachedCells: number;
|
|
50
49
|
|
|
51
50
|
constructor(options: EvalDetachedCellManagerOptions = {}) {
|
|
52
|
-
this.#artifactsDir = options.artifactsDir;
|
|
53
51
|
this.#onStatusChange = options.onStatusChange;
|
|
54
52
|
this.#onWakeSourceState = options.onWakeSourceState;
|
|
55
53
|
this.#notificationQueue = new DetachedNotificationQueue(options.notifier);
|
|
@@ -85,7 +83,6 @@ export class EvalDetachedCellManager {
|
|
|
85
83
|
const cell = createManagedCell({
|
|
86
84
|
cellId,
|
|
87
85
|
input,
|
|
88
|
-
artifactsDir: this.#artifactsDir,
|
|
89
86
|
now: this.#now,
|
|
90
87
|
defaultHardLimitSeconds: this.#hardLimitSeconds,
|
|
91
88
|
defaultRunBudgetSeconds: this.#runBudgetSeconds,
|
|
@@ -239,7 +236,6 @@ export class EvalDetachedCellManager {
|
|
|
239
236
|
await cell.interruptOutcome?.promise;
|
|
240
237
|
return this.#snapshot(cell);
|
|
241
238
|
},
|
|
242
|
-
spillPath: cell.spillPath,
|
|
243
239
|
});
|
|
244
240
|
}
|
|
245
241
|
}
|
|
@@ -260,7 +256,7 @@ export class EvalDetachedCellManager {
|
|
|
260
256
|
return;
|
|
261
257
|
}
|
|
262
258
|
if (foreground) {
|
|
263
|
-
if (this.#settle(cell, "cancelled",
|
|
259
|
+
if (this.#settle(cell, "cancelled", cancelledDetachedResult(cell))) cell.onKill?.(expiry.error);
|
|
264
260
|
return;
|
|
265
261
|
}
|
|
266
262
|
await this.#cancel(cell, expiry.error.message);
|
|
@@ -269,7 +265,7 @@ export class EvalDetachedCellManager {
|
|
|
269
265
|
#cancelQueued(cell: ManagedCell, reason: string, error = new Error(reason)): void {
|
|
270
266
|
const dequeued = cell.kernel?.cancelQueued(cell.cellId, reason) ?? false;
|
|
271
267
|
cell.stateRetained = true;
|
|
272
|
-
this.#settle(cell, "cancelled",
|
|
268
|
+
this.#settle(cell, "cancelled", cancelledDetachedResult(cell));
|
|
273
269
|
// Acquisition/reset has no queue promise yet; its execution has no interrupt target either.
|
|
274
270
|
if (!dequeued) cell.onKill?.(error);
|
|
275
271
|
}
|
|
@@ -278,7 +274,7 @@ export class EvalDetachedCellManager {
|
|
|
278
274
|
const outcome = Promise.withResolvers<void>();
|
|
279
275
|
cell.interruptOutcome = outcome;
|
|
280
276
|
try {
|
|
281
|
-
if (!this.#settle(cell, "cancelled",
|
|
277
|
+
if (!this.#settle(cell, "cancelled", cancelledDetachedResult(cell)) || cell.kernel === undefined) return;
|
|
282
278
|
const handle = await cell.kernel.interrupt(reason, cell.cellId);
|
|
283
279
|
cell.interruptNote = handle.note;
|
|
284
280
|
cell.stateRetained = await handle.stateRetained;
|
|
@@ -1,61 +1,21 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
1
|
import type { EvalDetachedCellNotification, EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
|
|
4
2
|
import { interruptionStateNote, unknownInterruptionStateNote } from "./interrupt-note.ts";
|
|
5
3
|
import type { EvalKernelState, EvalMemoryDetails } from "./types.ts";
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export async function buildDetachedCellNotification(
|
|
15
|
-
snapshot: EvalDetachedCellSnapshot,
|
|
16
|
-
spillPath: string | undefined,
|
|
17
|
-
): Promise<EvalDetachedCellNotification> {
|
|
18
|
-
const body = notificationBody(snapshot);
|
|
19
|
-
const overflow = Buffer.byteLength(body, "utf8") > NOTIFICATION_TAIL_BYTES;
|
|
20
|
-
let spillNotice = "";
|
|
21
|
-
if (overflow && spillPath !== undefined) {
|
|
22
|
-
try {
|
|
23
|
-
await mkdir(dirname(spillPath), { recursive: true });
|
|
24
|
-
await writeFile(spillPath, body, "utf8");
|
|
25
|
-
// The agent read tool resolves plain paths only, so the notice must carry
|
|
26
|
-
// the absolute spill path, never the kernel-helper local:// scheme.
|
|
27
|
-
spillNotice = `\nBuffered output overflowed; full output: ${spillPath}`;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
-
spillNotice = `\nBuffered output overflow could not be spilled: ${message}`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return {
|
|
34
|
-
cellId: snapshot.cellId,
|
|
35
|
-
content: `${overflow ? notificationPreview(snapshot) : body}${overflow ? "\n[…notification tail capped…]" : ""}${spillNotice}`,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function notificationBody(cell: EvalDetachedCellSnapshot): string {
|
|
40
|
-
const outcome = outcomeOf(cell);
|
|
41
|
-
const resultText = textContent(cell);
|
|
42
|
-
const stateNote = stateNoteOf(cell);
|
|
43
|
-
return [
|
|
44
|
-
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcome}.`,
|
|
45
|
-
resultText.length === 0 ? "(no output)" : resultText,
|
|
46
|
-
`${stateNote}</system-reminder>`,
|
|
47
|
-
].join("\n");
|
|
5
|
+
/**
|
|
6
|
+
* A detached cell's completion carries the same text its result would have shown in the foreground: the output sink
|
|
7
|
+
* already bounded it (the configured head, the tail, the middle marker and the artifact notice), plus its images.
|
|
8
|
+
*/
|
|
9
|
+
export function buildDetachedCellNotification(snapshot: EvalDetachedCellSnapshot): EvalDetachedCellNotification {
|
|
10
|
+
const images = snapshot.result.content.filter((part) => part.type === "image");
|
|
11
|
+
return { cellId: snapshot.cellId, content: notificationText(snapshot, textContent(snapshot)), images };
|
|
48
12
|
}
|
|
49
13
|
|
|
50
|
-
function
|
|
51
|
-
const outcome = outcomeOf(cell);
|
|
52
|
-
const resultText = textContent(cell);
|
|
53
|
-
const stateNote = stateNoteOf(cell);
|
|
14
|
+
function notificationText(cell: EvalDetachedCellSnapshot, output: string): string {
|
|
54
15
|
return [
|
|
55
|
-
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${
|
|
56
|
-
"
|
|
57
|
-
|
|
58
|
-
`${stateNote}</system-reminder>`,
|
|
16
|
+
`<system-reminder>Detached eval cell ${cell.cellId} (${cell.language}) ${outcomeOf(cell)}.`,
|
|
17
|
+
output.length === 0 ? "(no output)" : output,
|
|
18
|
+
`${stateNoteOf(cell)}</system-reminder>`,
|
|
59
19
|
].join("\n");
|
|
60
20
|
}
|
|
61
21
|
|
|
@@ -101,15 +61,3 @@ export function memoryStateNote(memory: EvalMemoryDetails | undefined): string {
|
|
|
101
61
|
return "The kernel was restarted before this cell ran - globals from earlier cells are gone; this cell's variables are available to the next eval cell.";
|
|
102
62
|
return "Kernel state updated - variables are available to the next eval cell.";
|
|
103
63
|
}
|
|
104
|
-
|
|
105
|
-
function safeCellId(cellId: string): string {
|
|
106
|
-
return cellId.replace(/[^a-zA-Z0-9_-]/gu, "_");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function truncateTailUtf8(text: string, maxBytes: number): string {
|
|
110
|
-
if (Buffer.byteLength(text, "utf8") <= maxBytes) return text;
|
|
111
|
-
const bytes = Buffer.from(text, "utf8");
|
|
112
|
-
let start = bytes.length - maxBytes;
|
|
113
|
-
while (start < bytes.length && (bytes[start] & 0xc0) === 0x80) start++;
|
|
114
|
-
return bytes.subarray(start).toString("utf8");
|
|
115
|
-
}
|
|
@@ -63,6 +63,23 @@ export function currentDetachedResult(cell: DetachedCellResultSource): AgentTool
|
|
|
63
63
|
return cell.terminalResult ?? cell.liveResult?.() ?? fallbackResult(cell.input);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The result a cell cancelled before its handler finished settles with: its buffered output, not the live progress
|
|
68
|
+
* frame ("1/1 cells running"), so a stopped cell never reads as still running.
|
|
69
|
+
*/
|
|
70
|
+
export function cancelledDetachedResult(cell: DetachedCellResultSource): AgentToolResult<EvalToolDetails> {
|
|
71
|
+
if (cell.terminalResult !== undefined) return cell.terminalResult;
|
|
72
|
+
const current = currentDetachedResult(cell);
|
|
73
|
+
const output = detachedOutputTail(current);
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{ type: "text", text: output.length > 0 ? output : "(no output)" },
|
|
77
|
+
...current.content.filter((part) => part.type === "image"),
|
|
78
|
+
],
|
|
79
|
+
details: current.details,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
export function detachedErrorResult(cell: DetachedCellResultSource, error: Error): AgentToolResult<EvalToolDetails> {
|
|
67
84
|
const current = currentDetachedResult(cell);
|
|
68
85
|
const output = detachedOutputTail(current);
|
|
@@ -46,9 +46,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
|
|
|
46
46
|
language: snapshot.language,
|
|
47
47
|
state: snapshot.state,
|
|
48
48
|
startedAtMs: snapshot.startedAtMs,
|
|
49
|
-
...(snapshot.queuedBehind === undefined
|
|
50
|
-
? {}
|
|
51
|
-
: { queuedBehind: [...snapshot.queuedBehind] }),
|
|
49
|
+
...(snapshot.queuedBehind === undefined ? {} : { queuedBehind: [...snapshot.queuedBehind] }),
|
|
52
50
|
...(summary ? { summary } : {}),
|
|
53
51
|
};
|
|
54
52
|
});
|
|
@@ -60,7 +58,7 @@ function createEvalListResult(cellManager: EvalDetachedCellManager): AgentToolRe
|
|
|
60
58
|
" ",
|
|
61
59
|
);
|
|
62
60
|
const elapsed = Math.floor(snapshot.result.details.durationMs / 1000);
|
|
63
|
-
const queued = cell.queuedBehind === undefined ? "" : `
|
|
61
|
+
const queued = cell.queuedBehind === undefined ? "" : ` ${queuedPhrase(cell.queuedBehind, cell.language)}`;
|
|
64
62
|
return `${cell.cellId} ${cell.language} ${cell.state} ${elapsed}s${queued} - ${preview}`;
|
|
65
63
|
})
|
|
66
64
|
.join("\n");
|
|
@@ -76,11 +74,13 @@ export function resultAfterDetach(
|
|
|
76
74
|
otherLiveCells: number,
|
|
77
75
|
): AgentToolResult<EvalToolDetails> {
|
|
78
76
|
if (snapshot.state !== "detached" && snapshot.state !== "running") return createDetachedControlResult(snapshot);
|
|
79
|
-
const
|
|
77
|
+
const queuedBehind = snapshot.queuedBehind;
|
|
80
78
|
const text =
|
|
81
|
-
|
|
79
|
+
queuedBehind === undefined
|
|
82
80
|
? `Eval cell ${snapshot.cellId} detached and is running in the ${input.language} kernel (${otherLiveCells} other live cells). Completion arrives as a notification; do not re-run it. eval({ action: "peek" | "stop", cell_id }) or eval({ action: "list" }).`
|
|
83
|
-
:
|
|
81
|
+
: queuedBehind.length === 0
|
|
82
|
+
? `Eval cell ${snapshot.cellId} is detached and waiting for the ${input.language} kernel to be ready; it runs first once the kernel is ready and completes as one notification. peek/stop/list with eval({ action, cell_id })`
|
|
83
|
+
: `Eval cell ${snapshot.cellId} is queued behind ${queuedBehind.join(", ")} in the ${input.language} kernel and detached; it runs after ${queuedBehind.join(", ")} and completes as one notification. peek/stop/list with eval({ action, cell_id })`;
|
|
84
84
|
return {
|
|
85
85
|
content: [
|
|
86
86
|
{
|
|
@@ -164,6 +164,12 @@ export function resultForDetachedState(
|
|
|
164
164
|
};
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
function queuedPhrase(queuedBehind: readonly string[], language: string): string {
|
|
168
|
+
return queuedBehind.length === 0
|
|
169
|
+
? `waiting for the ${language} kernel to be ready`
|
|
170
|
+
: `queued behind ${queuedBehind.join(", ")}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
167
173
|
function textContent(result: AgentToolResult<EvalToolDetails>): string {
|
|
168
174
|
return result.content
|
|
169
175
|
.filter((part) => part.type === "text")
|
|
@@ -3,7 +3,6 @@ import { buildDetachedCellNotification } from "./detached-cell-notification.ts";
|
|
|
3
3
|
|
|
4
4
|
export interface PendingDetachedNotification {
|
|
5
5
|
readonly snapshot: () => EvalDetachedCellSnapshot | Promise<EvalDetachedCellSnapshot>;
|
|
6
|
-
readonly spillPath: string | undefined;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
export class DetachedNotificationQueue {
|
|
@@ -30,7 +29,7 @@ export class DetachedNotificationQueue {
|
|
|
30
29
|
const flush = Promise.resolve().then(async () => {
|
|
31
30
|
const pending = this.#pending.splice(0);
|
|
32
31
|
const notifications = await Promise.all(
|
|
33
|
-
pending.map(async (item) =>
|
|
32
|
+
pending.map(async (item) => buildDetachedCellNotification(await item.snapshot())),
|
|
34
33
|
);
|
|
35
34
|
this.#notifier?.notify(notifications);
|
|
36
35
|
});
|
package/src/tool/eval-request.ts
CHANGED
|
@@ -17,9 +17,19 @@ export function normalizeEvalSummary(value: unknown): string | undefined {
|
|
|
17
17
|
return normalized.length === 0 ? undefined : normalized;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export class EvalIsolateInvalidError extends TypeError {
|
|
21
|
+
readonly name = "EvalIsolateInvalidError";
|
|
22
|
+
readonly code = "eval_isolate_invalid";
|
|
23
|
+
|
|
24
|
+
constructor(reason: string) {
|
|
25
|
+
super(`eval_isolate_invalid: ${reason}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export function parseEvalRequest(
|
|
21
30
|
params: unknown,
|
|
22
31
|
enabledLanguages: readonly EvalLanguage[] = evalLanguageOrder,
|
|
32
|
+
options: { readonly sandbox?: boolean } = {},
|
|
23
33
|
): EvalToolRequest {
|
|
24
34
|
if (!isRecord(params)) throw new TypeError("eval parameters must be an object");
|
|
25
35
|
if (params.action === "list") return { action: "list" };
|
|
@@ -30,7 +40,9 @@ export function parseEvalRequest(
|
|
|
30
40
|
}
|
|
31
41
|
if (params.action !== undefined && params.action !== "run")
|
|
32
42
|
throw new TypeError(`Unknown eval action "${String(params.action)}"`);
|
|
33
|
-
if (
|
|
43
|
+
if (params.language === undefined) throw new TypeError(evalRunRequiresLanguageMessage(enabledLanguages));
|
|
44
|
+
if (!isEvalLanguage(params.language))
|
|
45
|
+
throw new TypeError(`eval run language must be one of: ${enabledLanguages.join(", ")}`);
|
|
34
46
|
if (typeof params.code !== "string")
|
|
35
47
|
throw new TypeError("eval run requires code — the cell body to execute, verbatim");
|
|
36
48
|
const summary = normalizeEvalSummary(params.summary);
|
|
@@ -40,6 +52,15 @@ export function parseEvalRequest(
|
|
|
40
52
|
);
|
|
41
53
|
if (params.on_timeout !== undefined && params.on_timeout !== "detach" && params.on_timeout !== "error")
|
|
42
54
|
throw new TypeError(`Unknown eval on_timeout value "${String(params.on_timeout)}"`);
|
|
55
|
+
if (params.isolate !== undefined && params.isolate !== false) {
|
|
56
|
+
if (params.isolate !== true) throw new EvalIsolateInvalidError("isolate must be true or false");
|
|
57
|
+
if (options.sandbox !== true) {
|
|
58
|
+
throw new EvalIsolateInvalidError("sandbox cells are turned off (set sandbox.enabled in codemode settings)");
|
|
59
|
+
}
|
|
60
|
+
if (params.language !== "js")
|
|
61
|
+
throw new EvalIsolateInvalidError('sandbox cells run JavaScript only (language: "js")');
|
|
62
|
+
if (params.reset === true) throw new EvalIsolateInvalidError("a sandbox cell starts fresh; reset does not apply");
|
|
63
|
+
}
|
|
43
64
|
return {
|
|
44
65
|
language: params.language,
|
|
45
66
|
code: params.code,
|
|
@@ -48,6 +69,7 @@ export function parseEvalRequest(
|
|
|
48
69
|
...(typeof params.timeout === "number" ? { timeout: params.timeout } : {}),
|
|
49
70
|
...(params.on_timeout === "detach" || params.on_timeout === "error" ? { on_timeout: params.on_timeout } : {}),
|
|
50
71
|
...(typeof params.reset === "boolean" ? { reset: params.reset } : {}),
|
|
72
|
+
...(params.isolate === true ? { isolate: true } : {}),
|
|
51
73
|
};
|
|
52
74
|
}
|
|
53
75
|
|
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
9
9
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
10
10
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
11
|
+
import type { JsEnvironments } from "../environments/js-environments.ts";
|
|
12
|
+
import type { PythonEnvironments } from "../environments/python-environments.ts";
|
|
11
13
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
12
14
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
13
15
|
import type { EvalTimeoutFactory } from "./cell-execution.ts";
|
|
@@ -69,6 +71,10 @@ export interface CreateEvalToolOptions {
|
|
|
69
71
|
readonly bunSkillPath?: string;
|
|
70
72
|
/** Kernel globals of the tools active when a cell is submitted; read once per cell. */
|
|
71
73
|
readonly kernelPreludes?: () => readonly KernelPreludeContribution[];
|
|
74
|
+
/** The session's Python environments: `%pip` / `%environment` cells and the import root of every Python cell. */
|
|
75
|
+
readonly pythonEnvironments?: PythonEnvironments;
|
|
76
|
+
/** The session's managed JavaScript packages: `%bun add` / `%npm add` cells and the bare-import fallback root. */
|
|
77
|
+
readonly jsEnvironments?: JsEnvironments;
|
|
72
78
|
/** Contributions whose documentation lines the description lists; snapshot taken when the tool is (re)registered. */
|
|
73
79
|
readonly promptKernelPreludes?: readonly KernelPreludeContribution[];
|
|
74
80
|
}
|
package/src/tool/eval-tool.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult, ToolDefinition } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { TUnsafe } from "typebox";
|
|
3
|
+
import { resolveAdvertiseHelpers, resolveSandbox } from "../config/feature-settings.ts";
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_FOREGROUND_WINDOW_SECONDS,
|
|
5
6
|
defaultCodemodeSettings,
|
|
@@ -8,7 +9,12 @@ import {
|
|
|
8
9
|
import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
|
|
9
10
|
import { EvalDetachedCellManager } from "./detached-cell-manager.ts";
|
|
10
11
|
import { executeEvalControl } from "./detached-eval-result.ts";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
EvalIsolateInvalidError,
|
|
14
|
+
isEvalControlRequest,
|
|
15
|
+
normalizeEvalSummary,
|
|
16
|
+
parseEvalRequest,
|
|
17
|
+
} from "./eval-request.ts";
|
|
12
18
|
import type { CreateEvalToolOptions } from "./eval-tool-options.ts";
|
|
13
19
|
import { runEvalCell } from "./run-eval-cell.ts";
|
|
14
20
|
import {
|
|
@@ -43,9 +49,11 @@ export function createEvalTool(options: CreateEvalToolOptions) {
|
|
|
43
49
|
foregroundWindowSeconds,
|
|
44
50
|
hardLimitSeconds: options.hardLimitSeconds ?? defaultEvalDeadlineSeconds.hardLimitSeconds,
|
|
45
51
|
};
|
|
46
|
-
const
|
|
52
|
+
const sandbox = resolveSandbox(options.settings ?? defaultCodemodeSettings).enabled;
|
|
53
|
+
const parameters = createEvalInputSchema(options.enabledLanguages, deadlines, { sandbox });
|
|
47
54
|
const prompt = buildEvalPrompt(options.enabledLanguages, {
|
|
48
55
|
spawns: options.spawns ?? false,
|
|
56
|
+
...(resolveAdvertiseHelpers(options.settings ?? defaultCodemodeSettings) ? { advertiseHelpers: true } : {}),
|
|
49
57
|
monitor: options.monitor,
|
|
50
58
|
maxDetachedCells,
|
|
51
59
|
runBudgetSeconds: deadlines.runBudgetSeconds,
|
|
@@ -74,9 +82,14 @@ export function createEvalTool(options: CreateEvalToolOptions) {
|
|
|
74
82
|
async function execute(
|
|
75
83
|
...[toolCallId, params, signal, onUpdate, ctx]: EvalExecuteArgs<EvalToolRequest>
|
|
76
84
|
): Promise<AgentToolResult<EvalResultDetails>> {
|
|
77
|
-
const request = parseEvalRequest(params, languages);
|
|
85
|
+
const request = parseEvalRequest(params, languages, { sandbox });
|
|
78
86
|
if (isEvalControlRequest(request)) return await executeEvalControl(cellManager, request);
|
|
79
|
-
if (options.proxyExecutor)
|
|
87
|
+
if (options.proxyExecutor) {
|
|
88
|
+
// A proxy runs cells elsewhere and knows nothing of sandbox cells: an isolated cell must never run unisolated.
|
|
89
|
+
if ("isolate" in request && request.isolate === true)
|
|
90
|
+
throw new EvalIsolateInvalidError("isolate: true is not available through this eval proxy");
|
|
91
|
+
return await options.proxyExecutor(request, signal);
|
|
92
|
+
}
|
|
80
93
|
if (!languages.includes(request.language))
|
|
81
94
|
throw new RangeError(
|
|
82
95
|
`Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
|
|
@@ -26,9 +26,8 @@ export async function describeTimeoutState(
|
|
|
26
26
|
);
|
|
27
27
|
if (outcome === undefined) error.message = fallbackTimeoutMessage(error.message);
|
|
28
28
|
else if (outcome.retained)
|
|
29
|
-
error.message = `${error.message} The kernel
|
|
30
|
-
else
|
|
31
|
-
error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
|
|
29
|
+
error.message = `${error.message} The kernel was not restarted; variables from earlier cells are kept.`;
|
|
30
|
+
else error.message = `${error.message} The kernel was restarted; variables from earlier cells are lost.`;
|
|
32
31
|
if (outcome?.note !== undefined) error.message = `${error.message} ${outcome.note.trim()}`;
|
|
33
32
|
return error;
|
|
34
33
|
}
|
|
@@ -63,8 +62,8 @@ const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
|
|
|
63
62
|
export function interruptionStateNote(language: EvalLanguage, stateRetained: boolean | undefined): string | undefined {
|
|
64
63
|
if (stateRetained === undefined) return undefined;
|
|
65
64
|
const label = LANGUAGE_LABEL[language];
|
|
66
|
-
if (stateRetained) return
|
|
67
|
-
return
|
|
65
|
+
if (stateRetained) return `The ${label} was not restarted; variables from earlier cells are kept.`;
|
|
66
|
+
return `The ${label} was restarted; variables from earlier cells are lost.`;
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
export function unknownInterruptionStateNote(language: EvalLanguage): string {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { sanitizeTerminalLabel, visibleWidth } from "@code-yeongyu/senpi";
|
|
2
|
+
import { type EvalCellResult, type EvalLanguage, evalLanguageOrder } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
const HEADLINED_STATUSES: ReadonlySet<EvalCellResult["status"]> = new Set(["pending", "queued", "running", "detached"]);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A collapsed headline never wraps the frame: it gets what the language, state and timing leave on the line. This
|
|
8
|
+
* floor (in screen cells) only keeps a word or two visible in very narrow terminals.
|
|
9
|
+
*/
|
|
10
|
+
const MIN_HEADLINE_CELLS = 8;
|
|
11
|
+
|
|
12
|
+
export function leadsWithHeadline(status: EvalCellResult["status"]): boolean {
|
|
13
|
+
return HEADLINED_STATUSES.has(status);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What an in-progress cell is doing, in one line: its summary, or else the first line of its code that has content once sanitized (the
|
|
18
|
+
* oh-my-pi fallback), or an ellipsis while the arguments are still streaming in. Collapsed rows cut it to
|
|
19
|
+
* `maxCells` screen cells (a wide character takes two), so the headline stays one line. Whichever is chosen is
|
|
20
|
+
* sanitized before it is measured, so escape and control characters never reach the row (senpi#2831, senpi#2839).
|
|
21
|
+
*/
|
|
22
|
+
export function liveHeadline(
|
|
23
|
+
summary: string | undefined,
|
|
24
|
+
code: string | undefined,
|
|
25
|
+
maxCells: number | undefined,
|
|
26
|
+
): string {
|
|
27
|
+
const text = (summary === undefined ? "" : sanitizeTerminalLabel(summary)) || firstCodeLine(code) || "…";
|
|
28
|
+
const limit = maxCells === undefined ? undefined : Math.max(MIN_HEADLINE_CELLS, maxCells);
|
|
29
|
+
if (limit === undefined || visibleWidth(text) <= limit) return text;
|
|
30
|
+
return `${cellPrefix(text, limit - 1)}…`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// A plain cut by screen cells: the headline is styled by the caller, so no reset codes may be inserted here.
|
|
34
|
+
function cellPrefix(text: string, cells: number): string {
|
|
35
|
+
let kept = "";
|
|
36
|
+
let used = 0;
|
|
37
|
+
for (const { segment } of new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)) {
|
|
38
|
+
const width = visibleWidth(segment);
|
|
39
|
+
if (used + width > cells) break;
|
|
40
|
+
kept += segment;
|
|
41
|
+
used += width;
|
|
42
|
+
}
|
|
43
|
+
return kept;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function knownLanguage(language: unknown): EvalLanguage | undefined {
|
|
47
|
+
return evalLanguageOrder.find((known) => known === language);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The first line with something to show once it is sanitized: a line of only escape or control characters is skipped
|
|
51
|
+
// (senpi#2850), and the later lines never reach the row.
|
|
52
|
+
function firstCodeLine(code: string | undefined): string | undefined {
|
|
53
|
+
if (code === undefined) return undefined;
|
|
54
|
+
// Stops at the first line with content, so a long cell is not sanitized line by line on every frame.
|
|
55
|
+
for (const line of code.split("\n")) {
|
|
56
|
+
const clean = sanitizeTerminalLabel(line);
|
|
57
|
+
if (clean.length > 0) return clean;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|