@code-yeongyu/senpi-codemode 2026.7.25-2 → 2026.7.26
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 +23 -0
- package/README.md +39 -19
- package/package.json +3 -3
- package/src/bridge/http-server.ts +6 -1
- package/src/bridges/output-bridge.ts +0 -1
- package/src/extension/eval-notifier.ts +40 -0
- package/src/index.ts +46 -66
- package/src/kernels/js/context-manager.ts +5 -2
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +19 -4
- package/src/kernels/py/prelude.py +7 -0
- package/src/kernels/shared/subprocess-kernel.ts +8 -5
- package/src/prompt/eval-prompt.ts +23 -5
- package/src/tool/detached-cell-manager.ts +322 -0
- package/src/tool/eval-tool.ts +253 -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 +65 -22
- package/src/codemode/runtime.ts +0 -258
- package/src/codemode/tools.ts +0 -106
package/src/tool/eval-tool.ts
CHANGED
|
@@ -6,23 +6,32 @@ import { defaultCodemodeSettings, type ResolvedCodemodeSettings } from "../confi
|
|
|
6
6
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
7
7
|
import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
|
|
8
8
|
import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
|
|
9
|
-
import { IdleTimeout } from "../timeouts/idle-timeout.ts";
|
|
9
|
+
import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
|
|
10
10
|
import { CellHandler, type CellState } from "./cell-handler.ts";
|
|
11
|
+
import { EvalDetachedCellManager, type EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
|
|
11
12
|
import type { EvalImageResizer } from "./image.ts";
|
|
13
|
+
import { describeTimeoutState, interruptionStateNote } from "./interrupt-note.ts";
|
|
12
14
|
import {
|
|
13
15
|
createEvalInputSchema,
|
|
14
16
|
type EnabledEvalLanguages,
|
|
17
|
+
type EvalCellResult,
|
|
18
|
+
type EvalControlInput,
|
|
15
19
|
type EvalInputSchema,
|
|
16
20
|
type EvalKernel,
|
|
17
21
|
type EvalKernelManager,
|
|
18
22
|
type EvalToolDetails,
|
|
19
23
|
type EvalToolInput,
|
|
24
|
+
type EvalToolRequest,
|
|
20
25
|
type ExecuteTool,
|
|
21
26
|
enabledLanguageList,
|
|
22
27
|
} from "./types.ts";
|
|
23
28
|
|
|
24
29
|
export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./types.ts";
|
|
25
30
|
|
|
31
|
+
export interface EvalTimeoutFactory {
|
|
32
|
+
create(options: IdleTimeoutOptions): TimeoutPauseHandle & { dispose(): void };
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export interface CreateEvalToolOptions {
|
|
27
36
|
readonly enabledLanguages: EnabledEvalLanguages;
|
|
28
37
|
readonly kernelManager: EvalKernelManager;
|
|
@@ -33,6 +42,8 @@ export interface CreateEvalToolOptions {
|
|
|
33
42
|
readonly artifactsDir?: string;
|
|
34
43
|
readonly imageResizer?: EvalImageResizer;
|
|
35
44
|
readonly executionTracker?: EvalExecutionTracker;
|
|
45
|
+
readonly cellManager?: EvalDetachedCellManager;
|
|
46
|
+
readonly timeoutFactory?: EvalTimeoutFactory;
|
|
36
47
|
readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
|
|
37
48
|
readonly renderers?: Pick<ToolDefinition<EvalInputSchema, EvalToolDetails>, "renderCall" | "renderResult">;
|
|
38
49
|
/** Whether the task-tool spawn helpers (agent()/output()/<dag>) are advertised in the prompt. */
|
|
@@ -56,18 +67,29 @@ interface EvalCellInvocation {
|
|
|
56
67
|
interface CellExecutionOptions {
|
|
57
68
|
readonly callerSignal: AbortSignal;
|
|
58
69
|
readonly cellId: string;
|
|
59
|
-
readonly onAbort: (error: Error) => void;
|
|
60
70
|
readonly timeoutMs: number;
|
|
71
|
+
readonly timeoutFactory: EvalTimeoutFactory;
|
|
72
|
+
readonly onTimeout: (error: Error) => void;
|
|
73
|
+
readonly onAbort: (error: Error) => void;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
const INTERRUPT_DELIVERY_GRACE_MS = 100;
|
|
77
|
+
const NON_INTERACTIVE_MODES = new Set(["print", "json"]);
|
|
78
|
+
|
|
79
|
+
const defaultTimeoutFactory: EvalTimeoutFactory = {
|
|
80
|
+
create(options): IdleTimeout {
|
|
81
|
+
return new IdleTimeout(options);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
64
84
|
|
|
65
85
|
class CellExecution {
|
|
66
86
|
readonly #callerSignal: AbortSignal;
|
|
67
87
|
readonly #onAbort: (error: Error) => void;
|
|
68
88
|
readonly #abortPromise: Promise<never>;
|
|
69
|
-
readonly #
|
|
89
|
+
readonly #detachedPromise: Promise<void>;
|
|
90
|
+
readonly #watchdog: TimeoutPauseHandle & { dispose(): void };
|
|
70
91
|
#rejectAbort: ((reason?: unknown) => void) | undefined;
|
|
92
|
+
#resolveDetached: (() => void) | undefined;
|
|
71
93
|
#kernel: EvalKernel | undefined;
|
|
72
94
|
#interruptDeadline: ReturnType<typeof setTimeout> | undefined;
|
|
73
95
|
#active = true;
|
|
@@ -78,26 +100,44 @@ class CellExecution {
|
|
|
78
100
|
this.#abortPromise = new Promise<never>((_resolve, reject) => {
|
|
79
101
|
this.#rejectAbort = reject;
|
|
80
102
|
});
|
|
81
|
-
this.#
|
|
103
|
+
this.#detachedPromise = new Promise<void>((resolve) => {
|
|
104
|
+
this.#resolveDetached = resolve;
|
|
105
|
+
});
|
|
106
|
+
this.#watchdog = options.timeoutFactory.create({
|
|
82
107
|
cellId: options.cellId,
|
|
83
108
|
timeoutMs: options.timeoutMs,
|
|
84
|
-
onTimeout: ({ error }) =>
|
|
109
|
+
onTimeout: ({ error }) => options.onTimeout(error),
|
|
85
110
|
});
|
|
86
111
|
this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, { once: true });
|
|
87
112
|
}
|
|
88
113
|
|
|
114
|
+
get detached(): Promise<void> {
|
|
115
|
+
return this.#detachedPromise;
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
pause(): void {
|
|
90
119
|
this.#watchdog.pause();
|
|
91
120
|
}
|
|
121
|
+
|
|
92
122
|
resume(): void {
|
|
93
123
|
this.#watchdog.resume();
|
|
94
124
|
}
|
|
125
|
+
|
|
95
126
|
setKernel(kernel: EvalKernel): void {
|
|
96
127
|
this.#kernel = kernel;
|
|
97
128
|
}
|
|
129
|
+
|
|
130
|
+
detach(): void {
|
|
131
|
+
if (!this.#active) return;
|
|
132
|
+
this.#watchdog.dispose();
|
|
133
|
+
this.#resolveDetached?.();
|
|
134
|
+
this.#resolveDetached = undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
98
137
|
cancel(reason: unknown): void {
|
|
99
138
|
this.#abort(reason);
|
|
100
139
|
}
|
|
140
|
+
|
|
101
141
|
finish(): void {
|
|
102
142
|
this.#active = false;
|
|
103
143
|
this.#cleanup();
|
|
@@ -115,6 +155,9 @@ class CellExecution {
|
|
|
115
155
|
this.#abort(this.#callerSignal.reason);
|
|
116
156
|
};
|
|
117
157
|
|
|
158
|
+
/** Outcome of the most recent interrupt, when a kernel was interrupted. */
|
|
159
|
+
interruptStateRetained: Promise<boolean> | undefined;
|
|
160
|
+
|
|
118
161
|
#abort(reason: unknown): void {
|
|
119
162
|
if (!this.#active) return;
|
|
120
163
|
this.#active = false;
|
|
@@ -128,7 +171,12 @@ class CellExecution {
|
|
|
128
171
|
}
|
|
129
172
|
this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
|
|
130
173
|
void Promise.resolve()
|
|
131
|
-
.then(() =>
|
|
174
|
+
.then(async () => {
|
|
175
|
+
const handle = await kernel.interrupt(error.message);
|
|
176
|
+
// Kernels predating the interrupt-outcome contract resolve void; leave
|
|
177
|
+
// the outcome undefined so callers report an honest unknown state.
|
|
178
|
+
this.interruptStateRetained = handle?.stateRetained;
|
|
179
|
+
})
|
|
132
180
|
.then(
|
|
133
181
|
() => this.#settleAbort(error),
|
|
134
182
|
(interruptError: unknown) => this.#settleAbort(interruptError),
|
|
@@ -160,6 +208,7 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
160
208
|
...(options.hostLine === undefined ? {} : { hostLine: options.hostLine }),
|
|
161
209
|
});
|
|
162
210
|
const languages = enabledLanguageList(options.enabledLanguages);
|
|
211
|
+
const cellManager = options.cellManager ?? new EvalDetachedCellManager({ artifactsDir: options.artifactsDir });
|
|
163
212
|
return {
|
|
164
213
|
name: "eval",
|
|
165
214
|
label: "Eval",
|
|
@@ -171,19 +220,23 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
171
220
|
...(options.renderers?.renderCall === undefined ? {} : { renderCall: options.renderers.renderCall }),
|
|
172
221
|
...(options.renderers?.renderResult === undefined ? {} : { renderResult: options.renderers.renderResult }),
|
|
173
222
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
174
|
-
|
|
175
|
-
if (
|
|
223
|
+
const request = requestFrom(params);
|
|
224
|
+
if (isControlRequest(request)) return await executeControl(cellManager, request);
|
|
225
|
+
if (options.proxyExecutor) return await options.proxyExecutor(request, signal);
|
|
226
|
+
if (!languages.includes(request.language))
|
|
176
227
|
throw new RangeError(
|
|
177
|
-
`Unsupported eval language "${
|
|
228
|
+
`Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
|
|
178
229
|
);
|
|
230
|
+
const busy = cellManager.busyFor(request.language);
|
|
231
|
+
if (busy !== undefined) throw kernelBusyError(busy);
|
|
179
232
|
options.executionTracker?.assertEvalExecutionAllowed();
|
|
180
233
|
const lifecycleController = new AbortController();
|
|
181
234
|
const combinedSignal = signal
|
|
182
235
|
? AbortSignal.any([signal, lifecycleController.signal])
|
|
183
236
|
: lifecycleController.signal;
|
|
184
|
-
const execution = runEvalCell(options, {
|
|
237
|
+
const execution = runEvalCell(options, cellManager, {
|
|
185
238
|
cellId: toolCallId,
|
|
186
|
-
input:
|
|
239
|
+
input: request,
|
|
187
240
|
signal: combinedSignal,
|
|
188
241
|
onUpdate,
|
|
189
242
|
ctx,
|
|
@@ -197,10 +250,12 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
|
|
|
197
250
|
|
|
198
251
|
async function runEvalCell(
|
|
199
252
|
options: CreateEvalToolOptions,
|
|
253
|
+
cellManager: EvalDetachedCellManager,
|
|
200
254
|
invocation: EvalCellInvocation,
|
|
201
255
|
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
202
256
|
if (invocation.signal.aborted) throw abortError(invocation.signal.reason);
|
|
203
257
|
const timeoutMs = Math.floor((invocation.input.timeout ?? options.cellTimeoutSeconds) * 1_000);
|
|
258
|
+
const timeoutBehavior = timeoutBehaviorFor(invocation.input, invocation.ctx);
|
|
204
259
|
const bridgeAbortController = new AbortController();
|
|
205
260
|
const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
|
|
206
261
|
const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
|
|
@@ -217,20 +272,68 @@ async function runEvalCell(
|
|
|
217
272
|
durationMs: 0,
|
|
218
273
|
status: "pending",
|
|
219
274
|
};
|
|
220
|
-
const
|
|
275
|
+
const cell = cellManager.create(invocation.cellId, invocation.input);
|
|
276
|
+
let execution: CellExecution;
|
|
277
|
+
execution = new CellExecution({
|
|
221
278
|
callerSignal: invocation.signal,
|
|
222
279
|
cellId: invocation.cellId,
|
|
280
|
+
timeoutMs,
|
|
281
|
+
timeoutFactory: options.timeoutFactory ?? defaultTimeoutFactory,
|
|
282
|
+
onTimeout: (error) => {
|
|
283
|
+
if (timeoutBehavior === "detach" && cellManager.detach(cell)) {
|
|
284
|
+
execution.detach();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
execution.cancel(error);
|
|
288
|
+
},
|
|
223
289
|
onAbort: (error) => {
|
|
224
290
|
state.active = false;
|
|
225
291
|
bridgeAbortController.abort(error);
|
|
226
292
|
},
|
|
227
|
-
timeoutMs,
|
|
228
293
|
});
|
|
294
|
+
const running = executeCell(
|
|
295
|
+
options,
|
|
296
|
+
invocation,
|
|
297
|
+
cellManager,
|
|
298
|
+
cell,
|
|
299
|
+
state,
|
|
300
|
+
execution,
|
|
301
|
+
bridgeContext,
|
|
302
|
+
bridgeAbortController,
|
|
303
|
+
);
|
|
304
|
+
const finalized = running.then(
|
|
305
|
+
(result) => {
|
|
306
|
+
cellManager.complete(cell, result);
|
|
307
|
+
return result;
|
|
308
|
+
},
|
|
309
|
+
(error: unknown) => {
|
|
310
|
+
cellManager.fail(cell, error instanceof Error ? error : new Error(String(error)));
|
|
311
|
+
throw error;
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
const outcome = await Promise.race([
|
|
315
|
+
finalized.then((result) => ({ kind: "result" as const, result })),
|
|
316
|
+
execution.detached.then(() => ({ kind: "detached" as const })),
|
|
317
|
+
]);
|
|
318
|
+
if (outcome.kind === "detached") return detachedResult(cellManager.peek(invocation.cellId), invocation.input);
|
|
319
|
+
return outcome.result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function executeCell(
|
|
323
|
+
options: CreateEvalToolOptions,
|
|
324
|
+
invocation: EvalCellInvocation,
|
|
325
|
+
cellManager: EvalDetachedCellManager,
|
|
326
|
+
cell: Parameters<EvalDetachedCellManager["markRunning"]>[0],
|
|
327
|
+
state: CellState,
|
|
328
|
+
execution: CellExecution,
|
|
329
|
+
bridgeContext: ExtensionContext,
|
|
330
|
+
bridgeAbortController: AbortController,
|
|
331
|
+
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
229
332
|
let handler: CellHandler | undefined;
|
|
230
333
|
try {
|
|
231
|
-
const
|
|
334
|
+
const kernel = await execution.wait(
|
|
232
335
|
options.kernelManager.getKernel(invocation.input.language, (message) => {
|
|
233
|
-
if (!state.active ||
|
|
336
|
+
if (!state.active || handler === undefined) return;
|
|
234
337
|
if (message.type === "status") {
|
|
235
338
|
if (message.event.op === TIMEOUT_PAUSE_OP) {
|
|
236
339
|
execution.pause();
|
|
@@ -245,7 +348,6 @@ async function runEvalCell(
|
|
|
245
348
|
void pending.catch((error: unknown) => execution.cancel(error));
|
|
246
349
|
}),
|
|
247
350
|
);
|
|
248
|
-
const kernel = acquired;
|
|
249
351
|
execution.setKernel(kernel);
|
|
250
352
|
handler = new CellHandler(kernel, state, {
|
|
251
353
|
executeTool: options.executeTool,
|
|
@@ -257,6 +359,7 @@ async function runEvalCell(
|
|
|
257
359
|
: { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
|
|
258
360
|
...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
|
|
259
361
|
});
|
|
362
|
+
cellManager.markRunning(cell, kernel, () => state.output);
|
|
260
363
|
if ("setContext" in options.kernelManager && typeof options.kernelManager.setContext === "function") {
|
|
261
364
|
options.kernelManager.setContext(bridgeContext);
|
|
262
365
|
}
|
|
@@ -265,9 +368,9 @@ async function runEvalCell(
|
|
|
265
368
|
if (result.ok && state.pendingBridgeCalls.length > 0) await execution.wait(Promise.all(state.pendingBridgeCalls));
|
|
266
369
|
return await handler.finalize(result);
|
|
267
370
|
} catch (error) {
|
|
268
|
-
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
371
|
+
if (handler && error instanceof Error && error.name === "CodemodeSessionDisposedError")
|
|
269
372
|
return await handler.finalizeCancellation(error);
|
|
270
|
-
|
|
373
|
+
if (error instanceof Error && error.name === "TimeoutError") throw await describeTimeoutState(error, execution);
|
|
271
374
|
throw error;
|
|
272
375
|
} finally {
|
|
273
376
|
state.active = false;
|
|
@@ -277,6 +380,138 @@ async function runEvalCell(
|
|
|
277
380
|
}
|
|
278
381
|
}
|
|
279
382
|
|
|
383
|
+
function requestFrom(params: unknown): EvalToolRequest {
|
|
384
|
+
if (typeof params !== "object" || params === null) throw new TypeError("eval parameters must be an object");
|
|
385
|
+
const value = params as Record<string, unknown>;
|
|
386
|
+
if (value.action === "peek" || value.action === "stop") {
|
|
387
|
+
if (typeof value.cell_id !== "string" || value.cell_id.length === 0)
|
|
388
|
+
throw new TypeError(`eval action "${value.action}" requires cell_id`);
|
|
389
|
+
return { action: value.action, cell_id: value.cell_id };
|
|
390
|
+
}
|
|
391
|
+
if (value.action !== undefined && value.action !== "run")
|
|
392
|
+
throw new TypeError(`Unknown eval action "${String(value.action)}"`);
|
|
393
|
+
if (!isEvalLanguage(value.language)) throw new TypeError("eval run requires language");
|
|
394
|
+
if (typeof value.code !== "string") throw new TypeError("eval run requires code");
|
|
395
|
+
if (value.on_timeout !== undefined && value.on_timeout !== "detach" && value.on_timeout !== "error")
|
|
396
|
+
throw new TypeError(`Unknown eval on_timeout value "${String(value.on_timeout)}"`);
|
|
397
|
+
return {
|
|
398
|
+
language: value.language,
|
|
399
|
+
code: value.code,
|
|
400
|
+
...(value.action === "run" ? { action: "run" as const } : {}),
|
|
401
|
+
...(typeof value.title === "string" ? { title: value.title } : {}),
|
|
402
|
+
...(typeof value.timeout === "number" ? { timeout: value.timeout } : {}),
|
|
403
|
+
...(value.on_timeout === "detach" || value.on_timeout === "error" ? { on_timeout: value.on_timeout } : {}),
|
|
404
|
+
...(typeof value.reset === "boolean" ? { reset: value.reset } : {}),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isControlRequest(request: EvalToolRequest): request is EvalControlInput {
|
|
409
|
+
return request.action === "peek" || request.action === "stop";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function isEvalLanguage(value: unknown): value is EvalToolInput["language"] {
|
|
413
|
+
return value === "py" || value === "js" || value === "rb" || value === "jl";
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function executeControl(
|
|
417
|
+
cellManager: EvalDetachedCellManager,
|
|
418
|
+
request: EvalControlInput,
|
|
419
|
+
): Promise<AgentToolResult<EvalToolDetails>> {
|
|
420
|
+
const snapshot =
|
|
421
|
+
request.action === "stop" ? await cellManager.stop(request.cell_id) : cellManager.peek(request.cell_id);
|
|
422
|
+
return snapshotResult(snapshot);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function detachedResult(snapshot: EvalDetachedCellSnapshot, input: EvalToolInput): AgentToolResult<EvalToolDetails> {
|
|
426
|
+
return {
|
|
427
|
+
content: [
|
|
428
|
+
{
|
|
429
|
+
type: "text",
|
|
430
|
+
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}" }).`,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
details: {
|
|
434
|
+
language: input.language,
|
|
435
|
+
languages: [input.language],
|
|
436
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
437
|
+
durationMs: 0,
|
|
438
|
+
toolCalls: [],
|
|
439
|
+
truncated: false,
|
|
440
|
+
statusEvents: [{ op: "detached", cellId: snapshot.cellId }],
|
|
441
|
+
cells: [
|
|
442
|
+
{
|
|
443
|
+
index: 0,
|
|
444
|
+
...(input.title === undefined ? {} : { title: input.title }),
|
|
445
|
+
code: input.code,
|
|
446
|
+
language: input.language,
|
|
447
|
+
output: snapshot.outputTail,
|
|
448
|
+
status: "detached",
|
|
449
|
+
statusEvents: [{ op: "detached", cellId: snapshot.cellId }],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function snapshotResult(snapshot: EvalDetachedCellSnapshot): AgentToolResult<EvalToolDetails> {
|
|
457
|
+
const terminationNote =
|
|
458
|
+
snapshot.state === "cancelled" ? interruptionStateNote(snapshot.language, snapshot.stateRetained) : undefined;
|
|
459
|
+
const text = [
|
|
460
|
+
`Eval cell ${snapshot.cellId} (${snapshot.language}) is ${snapshot.state}.`,
|
|
461
|
+
snapshot.outputTail.length === 0 ? "(no buffered output)" : snapshot.outputTail,
|
|
462
|
+
...(terminationNote === undefined ? [] : [terminationNote]),
|
|
463
|
+
].join("\n");
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text }],
|
|
466
|
+
details: {
|
|
467
|
+
language: snapshot.language,
|
|
468
|
+
languages: [snapshot.language],
|
|
469
|
+
durationMs: snapshot.result?.details.durationMs ?? 0,
|
|
470
|
+
toolCalls: snapshot.result?.details.toolCalls ?? [],
|
|
471
|
+
truncated: snapshot.result?.details.truncated ?? false,
|
|
472
|
+
...(snapshot.state === "failed" ? { isError: true } : {}),
|
|
473
|
+
statusEvents: [{ op: snapshot.state, cellId: snapshot.cellId }],
|
|
474
|
+
cells: [
|
|
475
|
+
{
|
|
476
|
+
index: 0,
|
|
477
|
+
code: "",
|
|
478
|
+
language: snapshot.language,
|
|
479
|
+
output: snapshot.outputTail,
|
|
480
|
+
status: cellStatus(snapshot.state),
|
|
481
|
+
statusEvents: [{ op: snapshot.state, cellId: snapshot.cellId }],
|
|
482
|
+
},
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function cellStatus(state: EvalDetachedCellSnapshot["state"]): EvalCellResult["status"] {
|
|
489
|
+
switch (state) {
|
|
490
|
+
case "running":
|
|
491
|
+
return "running";
|
|
492
|
+
case "detached":
|
|
493
|
+
return "detached";
|
|
494
|
+
case "completed":
|
|
495
|
+
return "complete";
|
|
496
|
+
case "failed":
|
|
497
|
+
return "error";
|
|
498
|
+
case "cancelled":
|
|
499
|
+
return "cancelled";
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function kernelBusyError(snapshot: EvalDetachedCellSnapshot): Error {
|
|
504
|
+
const tail = snapshot.outputTail.length === 0 ? "(no output yet)" : snapshot.outputTail;
|
|
505
|
+
return new Error(
|
|
506
|
+
`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}`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function timeoutBehaviorFor(input: EvalToolInput, ctx: ExtensionContext): "detach" | "error" {
|
|
511
|
+
if (input.on_timeout !== undefined) return input.on_timeout;
|
|
512
|
+
return NON_INTERACTIVE_MODES.has(ctx.mode) ? "error" : "detach";
|
|
513
|
+
}
|
|
514
|
+
|
|
280
515
|
function abortError(reason: unknown): Error {
|
|
281
516
|
if (reason instanceof Error && reason.name !== "AbortError") return reason;
|
|
282
517
|
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
|
}
|