@code-yeongyu/senpi-codemode 2026.7.25-2 → 2026.7.28
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 +39 -0
- package/README.md +45 -19
- package/package.json +3 -3
- package/src/bridge/http-server.ts +6 -1
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/output-bridge.ts +0 -1
- package/src/bridges/reserved-dispatch.ts +56 -0
- package/src/bridges/schema-bridge.ts +69 -0
- package/src/bridges/schema-hint.ts +140 -0
- package/src/extension/eval-notifier.ts +40 -0
- package/src/extension/runtime-factory.ts +3 -1
- package/src/index.ts +50 -66
- package/src/kernels/jl/prelude.jl +6 -0
- package/src/kernels/js/context-manager.ts +5 -2
- package/src/kernels/js/local-module-loader.ts +4 -0
- package/src/kernels/js/worker-runtime.js +6 -0
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +19 -4
- package/src/kernels/py/prelude.py +17 -0
- package/src/kernels/rb/prelude.rb +6 -0
- package/src/kernels/shared/subprocess-kernel.ts +8 -5
- package/src/prompt/eval-prompt.ts +28 -5
- package/src/tool/cell-handler.ts +20 -18
- package/src/tool/detached-cell-manager.ts +322 -0
- package/src/tool/eval-tool.ts +256 -18
- package/src/tool/interrupt-note.ts +58 -0
- package/src/tool/render.ts +28 -6
- package/src/tool/status-events.ts +20 -0
- package/src/tool/types.ts +66 -23
- package/src/codemode/runtime.ts +0 -258
- package/src/codemode/tools.ts +0 -106
package/src/tool/eval-tool.ts
CHANGED
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
|
|
4
|
+
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
4
5
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
5
6
|
import { defaultCodemodeSettings, type ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
6
7
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
7
8
|
import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
|
|
8
9
|
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
9
|
-
import { IdleTimeout } from "../timeouts/idle-timeout.ts";
|
|
10
|
+
import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
|
|
10
11
|
import { CellHandler, type CellState } from "./cell-handler.ts";
|
|
12
|
+
import { EvalDetachedCellManager, type EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
|
|
11
13
|
import type { EvalImageResizer } from "./image.ts";
|
|
14
|
+
import { describeTimeoutState, interruptionStateNote } from "./interrupt-note.ts";
|
|
12
15
|
import {
|
|
13
16
|
createEvalInputSchema,
|
|
14
17
|
type EnabledEvalLanguages,
|
|
18
|
+
type EvalCellResult,
|
|
19
|
+
type EvalControlInput,
|
|
15
20
|
type EvalInputSchema,
|
|
16
21
|
type EvalKernel,
|
|
17
22
|
type EvalKernelManager,
|
|
18
23
|
type EvalToolDetails,
|
|
19
24
|
type EvalToolInput,
|
|
25
|
+
type EvalToolRequest,
|
|
20
26
|
type ExecuteTool,
|
|
21
27
|
enabledLanguageList,
|
|
22
28
|
} from "./types.ts";
|
|
23
29
|
|
|
24
30
|
export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./types.ts";
|
|
25
31
|
|
|
32
|
+
export interface EvalTimeoutFactory {
|
|
33
|
+
create(options: IdleTimeoutOptions): TimeoutPauseHandle & { dispose(): void };
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
export interface CreateEvalToolOptions {
|
|
27
37
|
readonly enabledLanguages: EnabledEvalLanguages;
|
|
28
38
|
readonly kernelManager: EvalKernelManager;
|
|
29
39
|
readonly cellTimeoutSeconds: number;
|
|
30
40
|
readonly executeTool: ExecuteTool;
|
|
41
|
+
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
31
42
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
32
43
|
readonly settings?: ResolvedCodemodeSettings;
|
|
33
44
|
readonly artifactsDir?: string;
|
|
34
45
|
readonly imageResizer?: EvalImageResizer;
|
|
35
46
|
readonly executionTracker?: EvalExecutionTracker;
|
|
47
|
+
readonly cellManager?: EvalDetachedCellManager;
|
|
48
|
+
readonly timeoutFactory?: EvalTimeoutFactory;
|
|
36
49
|
readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
|
|
37
50
|
readonly renderers?: Pick<ToolDefinition<EvalInputSchema, EvalToolDetails>, "renderCall" | "renderResult">;
|
|
38
51
|
/** Whether the task-tool spawn helpers (agent()/output()/<dag>) are advertised in the prompt. */
|
|
@@ -56,18 +69,29 @@ interface EvalCellInvocation {
|
|
|
56
69
|
interface CellExecutionOptions {
|
|
57
70
|
readonly callerSignal: AbortSignal;
|
|
58
71
|
readonly cellId: string;
|
|
59
|
-
readonly onAbort: (error: Error) => void;
|
|
60
72
|
readonly timeoutMs: number;
|
|
73
|
+
readonly timeoutFactory: EvalTimeoutFactory;
|
|
74
|
+
readonly onTimeout: (error: Error) => void;
|
|
75
|
+
readonly onAbort: (error: Error) => void;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
const INTERRUPT_DELIVERY_GRACE_MS = 100;
|
|
79
|
+
const NON_INTERACTIVE_MODES = new Set(["print", "json"]);
|
|
80
|
+
|
|
81
|
+
const defaultTimeoutFactory: EvalTimeoutFactory = {
|
|
82
|
+
create(options): IdleTimeout {
|
|
83
|
+
return new IdleTimeout(options);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
64
86
|
|
|
65
87
|
class CellExecution {
|
|
66
88
|
readonly #callerSignal: AbortSignal;
|
|
67
89
|
readonly #onAbort: (error: Error) => void;
|
|
68
90
|
readonly #abortPromise: Promise<never>;
|
|
69
|
-
readonly #
|
|
91
|
+
readonly #detachedPromise: Promise<void>;
|
|
92
|
+
readonly #watchdog: TimeoutPauseHandle & { dispose(): void };
|
|
70
93
|
#rejectAbort: ((reason?: unknown) => void) | undefined;
|
|
94
|
+
#resolveDetached: (() => void) | undefined;
|
|
71
95
|
#kernel: EvalKernel | undefined;
|
|
72
96
|
#interruptDeadline: ReturnType<typeof setTimeout> | undefined;
|
|
73
97
|
#active = true;
|
|
@@ -78,26 +102,44 @@ class CellExecution {
|
|
|
78
102
|
this.#abortPromise = new Promise<never>((_resolve, reject) => {
|
|
79
103
|
this.#rejectAbort = reject;
|
|
80
104
|
});
|
|
81
|
-
this.#
|
|
105
|
+
this.#detachedPromise = new Promise<void>((resolve) => {
|
|
106
|
+
this.#resolveDetached = resolve;
|
|
107
|
+
});
|
|
108
|
+
this.#watchdog = options.timeoutFactory.create({
|
|
82
109
|
cellId: options.cellId,
|
|
83
110
|
timeoutMs: options.timeoutMs,
|
|
84
|
-
onTimeout: ({ error }) =>
|
|
111
|
+
onTimeout: ({ error }) => options.onTimeout(error),
|
|
85
112
|
});
|
|
86
113
|
this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, { once: true });
|
|
87
114
|
}
|
|
88
115
|
|
|
116
|
+
get detached(): Promise<void> {
|
|
117
|
+
return this.#detachedPromise;
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
pause(): void {
|
|
90
121
|
this.#watchdog.pause();
|
|
91
122
|
}
|
|
123
|
+
|
|
92
124
|
resume(): void {
|
|
93
125
|
this.#watchdog.resume();
|
|
94
126
|
}
|
|
127
|
+
|
|
95
128
|
setKernel(kernel: EvalKernel): void {
|
|
96
129
|
this.#kernel = kernel;
|
|
97
130
|
}
|
|
131
|
+
|
|
132
|
+
detach(): void {
|
|
133
|
+
if (!this.#active) return;
|
|
134
|
+
this.#watchdog.dispose();
|
|
135
|
+
this.#resolveDetached?.();
|
|
136
|
+
this.#resolveDetached = undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
98
139
|
cancel(reason: unknown): void {
|
|
99
140
|
this.#abort(reason);
|
|
100
141
|
}
|
|
142
|
+
|
|
101
143
|
finish(): void {
|
|
102
144
|
this.#active = false;
|
|
103
145
|
this.#cleanup();
|
|
@@ -115,6 +157,9 @@ class CellExecution {
|
|
|
115
157
|
this.#abort(this.#callerSignal.reason);
|
|
116
158
|
};
|
|
117
159
|
|
|
160
|
+
/** Outcome of the most recent interrupt, when a kernel was interrupted. */
|
|
161
|
+
interruptStateRetained: Promise<boolean> | undefined;
|
|
162
|
+
|
|
118
163
|
#abort(reason: unknown): void {
|
|
119
164
|
if (!this.#active) return;
|
|
120
165
|
this.#active = false;
|
|
@@ -128,7 +173,12 @@ class CellExecution {
|
|
|
128
173
|
}
|
|
129
174
|
this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
|
|
130
175
|
void Promise.resolve()
|
|
131
|
-
.then(() =>
|
|
176
|
+
.then(async () => {
|
|
177
|
+
const handle = await kernel.interrupt(error.message);
|
|
178
|
+
// Kernels predating the interrupt-outcome contract resolve void; leave
|
|
179
|
+
// the outcome undefined so callers report an honest unknown state.
|
|
180
|
+
this.interruptStateRetained = handle?.stateRetained;
|
|
181
|
+
})
|
|
132
182
|
.then(
|
|
133
183
|
() => this.#settleAbort(error),
|
|
134
184
|
(interruptError: unknown) => this.#settleAbort(interruptError),
|
|
@@ -160,6 +210,7 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
160
210
|
...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
|
|
161
211
|
});
|
|
162
212
|
const languages = enabledLanguageList(options.enabledLanguages);
|
|
213
|
+
const cellManager = options.cellManager ?? new EvalDetachedCellManager({ artifactsDir: options.artifactsDir });
|
|
163
214
|
return {
|
|
164
215
|
name: "eval",
|
|
165
216
|
label: "Eval",
|
|
@@ -171,19 +222,23 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
171
222
|
...(options.renderers?.renderCall === undefined ? {} : { renderCall: options.renderers.renderCall }),
|
|
172
223
|
...(options.renderers?.renderResult === undefined ? {} : { renderResult: options.renderers.renderResult }),
|
|
173
224
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
174
|
-
|
|
175
|
-
if (
|
|
225
|
+
const request = requestFrom(params);
|
|
226
|
+
if (isControlRequest(request)) return await executeControl(cellManager, request);
|
|
227
|
+
if (options.proxyExecutor) return await options.proxyExecutor(request, signal);
|
|
228
|
+
if (!languages.includes(request.language))
|
|
176
229
|
throw new RangeError(
|
|
177
|
-
`Unsupported eval language "${
|
|
230
|
+
`Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
|
|
178
231
|
);
|
|
232
|
+
const busy = cellManager.busyFor(request.language);
|
|
233
|
+
if (busy !== undefined) throw kernelBusyError(busy);
|
|
179
234
|
options.executionTracker?.assertEvalExecutionAllowed();
|
|
180
235
|
const lifecycleController = new AbortController();
|
|
181
236
|
const combinedSignal = signal
|
|
182
237
|
? AbortSignal.any([signal, lifecycleController.signal])
|
|
183
238
|
: lifecycleController.signal;
|
|
184
|
-
const execution = runEvalCell(options, {
|
|
239
|
+
const execution = runEvalCell(options, cellManager, {
|
|
185
240
|
cellId: toolCallId,
|
|
186
|
-
input:
|
|
241
|
+
input: request,
|
|
187
242
|
signal: combinedSignal,
|
|
188
243
|
onUpdate,
|
|
189
244
|
ctx,
|
|
@@ -197,10 +252,12 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
197
252
|
|
|
198
253
|
async function runEvalCell(
|
|
199
254
|
options: CreateEvalToolOptions,
|
|
255
|
+
cellManager: EvalDetachedCellManager,
|
|
200
256
|
invocation: EvalCellInvocation,
|
|
201
257
|
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
202
258
|
if (invocation.signal.aborted) throw abortError(invocation.signal.reason);
|
|
203
259
|
const timeoutMs = Math.floor((invocation.input.timeout ?? options.cellTimeoutSeconds) * 1_000);
|
|
260
|
+
const timeoutBehavior = timeoutBehaviorFor(invocation.input, invocation.ctx);
|
|
204
261
|
const bridgeAbortController = new AbortController();
|
|
205
262
|
const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
|
|
206
263
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
@@ -217,20 +274,68 @@ async function runEvalCell(
|
|
|
217
274
|
durationMs: 0,
|
|
218
275
|
status: "pending",
|
|
219
276
|
};
|
|
220
|
-
const
|
|
277
|
+
const cell = cellManager.create(invocation.cellId, invocation.input);
|
|
278
|
+
let execution: CellExecution;
|
|
279
|
+
execution = new CellExecution({
|
|
221
280
|
callerSignal: invocation.signal,
|
|
222
281
|
cellId: invocation.cellId,
|
|
282
|
+
timeoutMs,
|
|
283
|
+
timeoutFactory: options.timeoutFactory ?? defaultTimeoutFactory,
|
|
284
|
+
onTimeout: (error) => {
|
|
285
|
+
if (timeoutBehavior === "detach" && cellManager.detach(cell)) {
|
|
286
|
+
execution.detach();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
execution.cancel(error);
|
|
290
|
+
},
|
|
223
291
|
onAbort: (error) => {
|
|
224
292
|
state.active = false;
|
|
225
293
|
bridgeAbortController.abort(error);
|
|
226
294
|
},
|
|
227
|
-
timeoutMs,
|
|
228
295
|
});
|
|
296
|
+
const running = executeCell(
|
|
297
|
+
options,
|
|
298
|
+
invocation,
|
|
299
|
+
cellManager,
|
|
300
|
+
cell,
|
|
301
|
+
state,
|
|
302
|
+
execution,
|
|
303
|
+
bridgeContext,
|
|
304
|
+
bridgeAbortController,
|
|
305
|
+
);
|
|
306
|
+
const finalized = running.then(
|
|
307
|
+
(result) => {
|
|
308
|
+
cellManager.complete(cell, result);
|
|
309
|
+
return result;
|
|
310
|
+
},
|
|
311
|
+
(error: unknown) => {
|
|
312
|
+
cellManager.fail(cell, error instanceof Error ? error : new Error(String(error)));
|
|
313
|
+
throw error;
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
const outcome = await Promise.race([
|
|
317
|
+
finalized.then((result) => ({ kind: "result" as const, result })),
|
|
318
|
+
execution.detached.then(() => ({ kind: "detached" as const })),
|
|
319
|
+
]);
|
|
320
|
+
if (outcome.kind === "detached") return detachedResult(cellManager.peek(invocation.cellId), invocation.input);
|
|
321
|
+
return outcome.result;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function executeCell(
|
|
325
|
+
options: CreateEvalToolOptions,
|
|
326
|
+
invocation: EvalCellInvocation,
|
|
327
|
+
cellManager: EvalDetachedCellManager,
|
|
328
|
+
cell: Parameters<EvalDetachedCellManager["markRunning"]>[0],
|
|
329
|
+
state: CellState,
|
|
330
|
+
execution: CellExecution,
|
|
331
|
+
bridgeContext: ExtensionContext,
|
|
332
|
+
bridgeAbortController: AbortController,
|
|
333
|
+
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
229
334
|
let handler: CellHandler | undefined;
|
|
230
335
|
try {
|
|
231
|
-
const
|
|
336
|
+
const kernel = await execution.wait(
|
|
232
337
|
options.kernelManager.getKernel(invocation.input.language, (message) => {
|
|
233
|
-
if (!state.active ||
|
|
338
|
+
if (!state.active || handler === undefined) return;
|
|
234
339
|
if (message.type === "status") {
|
|
235
340
|
if (message.event.op === TIMEOUT_PAUSE_OP) {
|
|
236
341
|
execution.pause();
|
|
@@ -245,10 +350,10 @@ async function runEvalCell(
|
|
|
245
350
|
void pending.catch((error: unknown) => execution.cancel(error));
|
|
246
351
|
}),
|
|
247
352
|
);
|
|
248
|
-
const kernel = acquired;
|
|
249
353
|
execution.setKernel(kernel);
|
|
250
354
|
handler = new CellHandler(kernel, state, {
|
|
251
355
|
executeTool: options.executeTool,
|
|
356
|
+
...(options.listTools === undefined ? {} : { listTools: options.listTools }),
|
|
252
357
|
settings: options.settings ?? defaultCodemodeSettings,
|
|
253
358
|
...(options.complete === undefined ? {} : { complete: options.complete }),
|
|
254
359
|
ctx: bridgeContext,
|
|
@@ -257,6 +362,7 @@ async function runEvalCell(
|
|
|
257
362
|
: { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
|
|
258
363
|
...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
|
|
259
364
|
});
|
|
365
|
+
cellManager.markRunning(cell, kernel, () => state.output);
|
|
260
366
|
if ("setContext" in options.kernelManager && typeof options.kernelManager.setContext === "function") {
|
|
261
367
|
options.kernelManager.setContext(bridgeContext);
|
|
262
368
|
}
|
|
@@ -265,9 +371,9 @@ async function runEvalCell(
|
|
|
265
371
|
if (result.ok && state.pendingBridgeCalls.length > 0) await execution.wait(Promise.all(state.pendingBridgeCalls));
|
|
266
372
|
return await handler.finalize(result);
|
|
267
373
|
} catch (error) {
|
|
268
|
-
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
374
|
+
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
269
375
|
return await handler.finalizeCancellation(error);
|
|
270
|
-
|
|
376
|
+
if (error instanceof Error && error.name === "TimeoutError") throw await describeTimeoutState(error, execution);
|
|
271
377
|
throw error;
|
|
272
378
|
} finally {
|
|
273
379
|
state.active = false;
|
|
@@ -277,6 +383,138 @@ async function runEvalCell(
|
|
|
277
383
|
}
|
|
278
384
|
}
|
|
279
385
|
|
|
386
|
+
function requestFrom(params: unknown): EvalToolRequest {
|
|
387
|
+
if (typeof params !== "object" || params === null) throw new TypeError("eval parameters must be an object");
|
|
388
|
+
const value = params as Record<string, unknown>;
|
|
389
|
+
if (value.action === "peek" || value.action === "stop") {
|
|
390
|
+
if (typeof value.cell_id !== "string" || value.cell_id.length === 0)
|
|
391
|
+
throw new TypeError(`eval action "${value.action}" requires cell_id`);
|
|
392
|
+
return { action: value.action, cell_id: value.cell_id };
|
|
393
|
+
}
|
|
394
|
+
if (value.action !== undefined && value.action !== "run")
|
|
395
|
+
throw new TypeError(`Unknown eval action "${String(value.action)}"`);
|
|
396
|
+
if (!isEvalLanguage(value.language)) throw new TypeError("eval run requires language");
|
|
397
|
+
if (typeof value.code !== "string") throw new TypeError("eval run requires code");
|
|
398
|
+
if (value.on_timeout !== undefined && value.on_timeout !== "detach" && value.on_timeout !== "error")
|
|
399
|
+
throw new TypeError(`Unknown eval on_timeout value "${String(value.on_timeout)}"`);
|
|
400
|
+
return {
|
|
401
|
+
language: value.language,
|
|
402
|
+
code: value.code,
|
|
403
|
+
...(value.action === "run" ? { action: "run" as const } : {}),
|
|
404
|
+
...(typeof value.title === "string" ? { title: value.title } : {}),
|
|
405
|
+
...(typeof value.timeout === "number" ? { timeout: value.timeout } : {}),
|
|
406
|
+
...(value.on_timeout === "detach" || value.on_timeout === "error" ? { on_timeout: value.on_timeout } : {}),
|
|
407
|
+
...(typeof value.reset === "boolean" ? { reset: value.reset } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isControlRequest(request: EvalToolRequest): request is EvalControlInput {
|
|
412
|
+
return request.action === "peek" || request.action === "stop";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isEvalLanguage(value: unknown): value is EvalToolInput["language"] {
|
|
416
|
+
return value === "py" || value === "js" || value === "rb" || value === "jl";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function executeControl(
|
|
420
|
+
cellManager: EvalDetachedCellManager,
|
|
421
|
+
request: EvalControlInput,
|
|
422
|
+
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
423
|
+
const snapshot =
|
|
424
|
+
request.action === "stop" ? await cellManager.stop(request.cell_id) : cellManager.peek(request.cell_id);
|
|
425
|
+
return snapshotResult(snapshot);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function detachedResult(snapshot: EvalDetachedCellSnapshot, input: EvalToolInput): AgentToolResult<EvalToolDetails> {
|
|
429
|
+
return {
|
|
430
|
+
content: [
|
|
431
|
+
{
|
|
432
|
+
type: "text",
|
|
433
|
+
text: `Eval cell ${snapshot.cellId} detached and is still running in the ${input.language} kernel. Completion will arrive as a notification. Use eval({ action: "peek", cell_id: "${snapshot.cellId}" }) or eval({ action: "stop", cell_id: "${snapshot.cellId}" }).`,
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
details: {
|
|
437
|
+
language: input.language,
|
|
438
|
+
languages: [input.language],
|
|
439
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
440
|
+
durationMs: 0,
|
|
441
|
+
toolCalls: [],
|
|
442
|
+
truncated: false,
|
|
443
|
+
statusEvents: [{ op: "detached", cellId: snapshot.cellId }],
|
|
444
|
+
cells: [
|
|
445
|
+
{
|
|
446
|
+
index: 0,
|
|
447
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
448
|
+
code: input.code,
|
|
449
|
+
language: input.language,
|
|
450
|
+
output: snapshot.outputTail,
|
|
451
|
+
status: "detached",
|
|
452
|
+
statusEvents: [{ op: "detached", cellId: snapshot.cellId }],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function snapshotResult(snapshot: EvalDetachedCellSnapshot): AgentToolResult<EvalToolDetails> {
|
|
460
|
+
const terminationNote =
|
|
461
|
+
snapshot.state === "cancelled" ? interruptionStateNote(snapshot.language, snapshot.stateRetained) : undefined;
|
|
462
|
+
const text = [
|
|
463
|
+
`Eval cell ${snapshot.cellId} (${snapshot.language}) is ${snapshot.state}.`,
|
|
464
|
+
snapshot.outputTail.length === 0 ? "(no buffered output)" : snapshot.outputTail,
|
|
465
|
+
...(terminationNote === undefined ? [] : [terminationNote]),
|
|
466
|
+
].join("\n");
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: "text", text }],
|
|
469
|
+
details: {
|
|
470
|
+
language: snapshot.language,
|
|
471
|
+
languages: [snapshot.language],
|
|
472
|
+
durationMs: snapshot.result?.details.durationMs ?? 0,
|
|
473
|
+
toolCalls: snapshot.result?.details.toolCalls ?? [],
|
|
474
|
+
truncated: snapshot.result?.details.truncated ?? false,
|
|
475
|
+
...(snapshot.state === "failed" ? { isError: true } : {}),
|
|
476
|
+
statusEvents: [{ op: snapshot.state, cellId: snapshot.cellId }],
|
|
477
|
+
cells: [
|
|
478
|
+
{
|
|
479
|
+
index: 0,
|
|
480
|
+
code: "",
|
|
481
|
+
language: snapshot.language,
|
|
482
|
+
output: snapshot.outputTail,
|
|
483
|
+
status: cellStatus(snapshot.state),
|
|
484
|
+
statusEvents: [{ op: snapshot.state, cellId: snapshot.cellId }],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function cellStatus(state: EvalDetachedCellSnapshot["state"]): EvalCellResult["status"] {
|
|
492
|
+
switch (state) {
|
|
493
|
+
case "running":
|
|
494
|
+
return "running";
|
|
495
|
+
case "detached":
|
|
496
|
+
return "detached";
|
|
497
|
+
case "completed":
|
|
498
|
+
return "complete";
|
|
499
|
+
case "failed":
|
|
500
|
+
return "error";
|
|
501
|
+
case "cancelled":
|
|
502
|
+
return "cancelled";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function kernelBusyError(snapshot: EvalDetachedCellSnapshot): Error {
|
|
507
|
+
const tail = snapshot.outputTail.length === 0 ? "(no output yet)" : snapshot.outputTail;
|
|
508
|
+
return new Error(
|
|
509
|
+
`The ${snapshot.language} eval kernel is busy running detached cell ${snapshot.cellId}. Do not re-run it; use eval({ action: "peek", cell_id: "${snapshot.cellId}" }). Current output tail:\n${tail}`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function timeoutBehaviorFor(input: EvalToolInput, ctx: ExtensionContext): "detach" | "error" {
|
|
514
|
+
if (input.on_timeout !== undefined) return input.on_timeout;
|
|
515
|
+
return NON_INTERACTIVE_MODES.has(ctx.mode) ? "error" : "detach";
|
|
516
|
+
}
|
|
517
|
+
|
|
280
518
|
function abortError(reason: unknown): Error {
|
|
281
519
|
if (reason instanceof Error && reason.name !== "AbortError") return reason;
|
|
282
520
|
const error = new Error(typeof reason === "string" ? reason : "Eval interrupted", { cause: reason });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { EvalLanguage } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const TIMEOUT_STATE_GRACE_MS = 5_500;
|
|
4
|
+
|
|
5
|
+
function fallbackTimeoutMessage(base: string): string {
|
|
6
|
+
return `${base} Kernel state may have been lost; re-establish any variables the next cell needs.`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Appends the kernel's actual post-timeout state to a TimeoutError, waiting a
|
|
11
|
+
* bounded window for the interrupt outcome so the model knows whether its
|
|
12
|
+
* variables survived. Falls back to an honest unknown when no outcome arrives.
|
|
13
|
+
*/
|
|
14
|
+
export async function describeTimeoutState(
|
|
15
|
+
error: Error,
|
|
16
|
+
execution: { readonly interruptStateRetained: Promise<boolean> | undefined },
|
|
17
|
+
): Promise<Error> {
|
|
18
|
+
const outcome = execution.interruptStateRetained;
|
|
19
|
+
if (outcome === undefined) {
|
|
20
|
+
error.message = fallbackTimeoutMessage(error.message);
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
const retained = await Promise.race([
|
|
25
|
+
outcome,
|
|
26
|
+
new Promise<boolean | undefined>((resolve) => {
|
|
27
|
+
timer = setTimeout(() => resolve(undefined), TIMEOUT_STATE_GRACE_MS);
|
|
28
|
+
}),
|
|
29
|
+
]).finally(() => {
|
|
30
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
31
|
+
});
|
|
32
|
+
if (retained === undefined) error.message = fallbackTimeoutMessage(error.message);
|
|
33
|
+
else if (retained)
|
|
34
|
+
error.message = `${error.message} The kernel remains running; its existing variables are preserved.`;
|
|
35
|
+
else
|
|
36
|
+
error.message = `${error.message} The kernel was unresponsive and restarted; variables from earlier cells are lost.`;
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LANGUAGE_LABEL: Record<EvalLanguage, string> = {
|
|
41
|
+
py: "Python kernel",
|
|
42
|
+
js: "JavaScript worker",
|
|
43
|
+
rb: "Ruby kernel",
|
|
44
|
+
jl: "Julia kernel",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Composes the user-facing note for a cancelled eval cell from the interrupt
|
|
49
|
+
* outcome the kernel actually reported — never a per-language assumption.
|
|
50
|
+
*
|
|
51
|
+
* Returns undefined when there is nothing truthful to add (no interrupt ran).
|
|
52
|
+
*/
|
|
53
|
+
export function interruptionStateNote(language: EvalLanguage, stateRetained: boolean | undefined): string | undefined {
|
|
54
|
+
if (stateRetained === undefined) return undefined;
|
|
55
|
+
const label = LANGUAGE_LABEL[language];
|
|
56
|
+
if (stateRetained) return `${label} was interrupted and remains running; its existing variables are preserved.`;
|
|
57
|
+
return `${label} was unresponsive to interrupt and was restarted; variables from earlier cells are lost.`;
|
|
58
|
+
}
|
package/src/tool/render.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
EvalStatusEvent,
|
|
27
27
|
EvalToolDetails,
|
|
28
28
|
EvalToolInput,
|
|
29
|
+
EvalToolRequest,
|
|
29
30
|
} from "./types.ts";
|
|
30
31
|
|
|
31
32
|
type EvalToolDefinition = ToolDefinition<EvalInputSchema, EvalToolDetails>;
|
|
@@ -202,7 +203,7 @@ type CellBadges = { readonly reset: boolean; readonly timeout: number | undefine
|
|
|
202
203
|
type PrefixStyle = { readonly prefix: string; readonly continuation: string; readonly color: ThemeColor };
|
|
203
204
|
type DetailedRenderContext = {
|
|
204
205
|
readonly environment: RenderEnvironment;
|
|
205
|
-
readonly args:
|
|
206
|
+
readonly args: EvalToolRequest;
|
|
206
207
|
readonly showImageFallback: boolean;
|
|
207
208
|
};
|
|
208
209
|
|
|
@@ -253,10 +254,14 @@ function cellPresentation(status: CellStatus, spinnerFrame: number | undefined):
|
|
|
253
254
|
return { label: "pending", icon: "○", color: "muted" };
|
|
254
255
|
case "running":
|
|
255
256
|
return { label: "running", icon: spinner(spinnerFrame), color: "warning" };
|
|
257
|
+
case "detached":
|
|
258
|
+
return { label: "detached", icon: "↗", color: "warning" };
|
|
256
259
|
case "complete":
|
|
257
260
|
return { label: "done", icon: "✓", color: "success" };
|
|
258
261
|
case "error":
|
|
259
262
|
return { label: "error", icon: "✗", color: "error" };
|
|
263
|
+
case "cancelled":
|
|
264
|
+
return { label: "cancelled", icon: "×", color: "error" };
|
|
260
265
|
default:
|
|
261
266
|
return assertNever(status);
|
|
262
267
|
}
|
|
@@ -404,6 +409,9 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme | undefined): st
|
|
|
404
409
|
case "phase":
|
|
405
410
|
parts.push(eventString(event.title) ?? "");
|
|
406
411
|
break;
|
|
412
|
+
case "status-events-omitted":
|
|
413
|
+
parts.push(`${eventNumber(event.count)} earlier events omitted`);
|
|
414
|
+
break;
|
|
407
415
|
default: {
|
|
408
416
|
if (event.count !== undefined) parts.push(String(event.count));
|
|
409
417
|
const path = eventString(event.path);
|
|
@@ -415,8 +423,13 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme | undefined): st
|
|
|
415
423
|
}
|
|
416
424
|
|
|
417
425
|
function renderStatusEvents(events: readonly EvalStatusEvent[], environment: RenderEnvironment): string[] {
|
|
418
|
-
|
|
419
|
-
|
|
426
|
+
// A bounded history stores its exact omission count in a leading marker event; fold that
|
|
427
|
+
// count into the summary line so collapsing the preview can never understate omissions.
|
|
428
|
+
const first = events[0];
|
|
429
|
+
const omittedByBound = first?.op === "status-events-omitted" && typeof first.count === "number" ? first.count : 0;
|
|
430
|
+
const visible = omittedByBound > 0 ? events.slice(1) : events;
|
|
431
|
+
const retained = environment.expanded ? visible : visible.slice(-STATUS_PREVIEW_COUNT);
|
|
432
|
+
const skipped = visible.length - retained.length + omittedByBound;
|
|
420
433
|
const lines: string[] = [];
|
|
421
434
|
if (skipped > 0) lines.push(style(environment.theme, "dim", `├ … ${skipped} earlier status events`));
|
|
422
435
|
for (const [index, event] of retained.entries()) {
|
|
@@ -613,9 +626,10 @@ function renderDetailedLines(
|
|
|
613
626
|
const lines: string[] = [];
|
|
614
627
|
const cells = details.cells ?? [];
|
|
615
628
|
for (const [index, cell] of cells.entries()) {
|
|
629
|
+
const run = isEvalRunInput(context.args) ? context.args : undefined;
|
|
616
630
|
const badges = {
|
|
617
|
-
reset: index === 0 &&
|
|
618
|
-
timeout: index === 0 ?
|
|
631
|
+
reset: index === 0 && run?.reset === true,
|
|
632
|
+
timeout: index === 0 ? run?.timeout : undefined,
|
|
619
633
|
};
|
|
620
634
|
appendLines(lines, renderCell(cell, context.environment, badges));
|
|
621
635
|
if (index < cells.length - 1) lines.push("");
|
|
@@ -668,6 +682,10 @@ function textOutput(result: AgentToolResult<EvalToolDetails>, showImageFallback:
|
|
|
668
682
|
return lines.join("\n");
|
|
669
683
|
}
|
|
670
684
|
|
|
685
|
+
function isEvalRunInput(args: EvalToolRequest): args is EvalToolInput {
|
|
686
|
+
return args.action !== "peek" && args.action !== "stop";
|
|
687
|
+
}
|
|
688
|
+
|
|
671
689
|
function toolCallRows(details: EvalToolDetails | undefined): ToolCallRow[] {
|
|
672
690
|
if (!details?.toolCalls || details.toolCalls.length === 0) return [];
|
|
673
691
|
return details.toolCalls.map((call) => {
|
|
@@ -720,7 +738,7 @@ function resultMetadata(
|
|
|
720
738
|
}
|
|
721
739
|
|
|
722
740
|
export function renderEvalCall(
|
|
723
|
-
args:
|
|
741
|
+
args: EvalToolRequest,
|
|
724
742
|
theme: Theme | undefined,
|
|
725
743
|
context: RenderContext,
|
|
726
744
|
): EvalRenderComponent {
|
|
@@ -731,6 +749,10 @@ export function renderEvalCall(
|
|
|
731
749
|
component.setBlocks([]);
|
|
732
750
|
return component;
|
|
733
751
|
}
|
|
752
|
+
if (!isEvalRunInput(args)) {
|
|
753
|
+
component.setBlocks([{ kind: "text", text: style(theme, "toolTitle", `eval ${args.action} ${args.cell_id}`) }]);
|
|
754
|
+
return component;
|
|
755
|
+
}
|
|
734
756
|
if (theme === undefined && context.spinnerFrame === undefined) {
|
|
735
757
|
const title = args.title === undefined ? "" : ` ${args.title}`;
|
|
736
758
|
const reset = args.reset === true ? " reset" : "";
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import type { EvalStatusEvent } from "./types.ts";
|
|
2
2
|
|
|
3
|
+
export const STATUS_EVENT_HISTORY_LIMIT = 100;
|
|
4
|
+
const OMITTED_STATUS_EVENTS_OP = "status-events-omitted";
|
|
5
|
+
|
|
6
|
+
function trimStatusHistory(events: EvalStatusEvent[]): void {
|
|
7
|
+
if (events.length <= STATUS_EVENT_HISTORY_LIMIT) return;
|
|
8
|
+
|
|
9
|
+
const first = events[0];
|
|
10
|
+
if (first?.op === OMITTED_STATUS_EVENTS_OP && typeof first.count === "number") {
|
|
11
|
+
const removeCount = events.length - STATUS_EVENT_HISTORY_LIMIT;
|
|
12
|
+
events.splice(1, removeCount);
|
|
13
|
+
events[0] = { op: OMITTED_STATUS_EVENTS_OP, count: first.count + removeCount };
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const removeCount = events.length - STATUS_EVENT_HISTORY_LIMIT + 1;
|
|
18
|
+
events.splice(0, removeCount, { op: OMITTED_STATUS_EVENTS_OP, count: removeCount });
|
|
19
|
+
}
|
|
20
|
+
|
|
3
21
|
export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
|
|
4
22
|
if (event.op === "agent" && typeof event.id === "string") {
|
|
5
23
|
const index = events.findIndex((candidate) => candidate.op === "agent" && candidate.id === event.id);
|
|
6
24
|
if (index >= 0) {
|
|
7
25
|
events[index] = event;
|
|
26
|
+
trimStatusHistory(events);
|
|
8
27
|
return;
|
|
9
28
|
}
|
|
10
29
|
}
|
|
11
30
|
events.push(event);
|
|
31
|
+
trimStatusHistory(events);
|
|
12
32
|
}
|