@code-yeongyu/senpi-codemode 2026.7.31 → 2026.8.1
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 +33 -0
- package/README.md +6 -3
- package/package.json +3 -3
- package/src/extension/eval-status-ticker.ts +79 -0
- package/src/extension/eval-status.ts +36 -3
- package/src/index.ts +26 -12
- package/src/tool/cell-execution.ts +146 -0
- package/src/tool/cell-handler.ts +21 -119
- package/src/tool/cell-runtime.ts +157 -0
- package/src/tool/detached-cell-manager.ts +62 -208
- package/src/tool/detached-cell-notification.ts +98 -0
- package/src/tool/detached-cell-snapshot.ts +81 -0
- package/src/tool/detached-cell-state.ts +24 -0
- package/src/tool/detached-eval-result.ts +140 -0
- package/src/tool/detached-notification-queue.ts +53 -0
- package/src/tool/eval-request.ts +45 -0
- package/src/tool/eval-tool-options.ts +45 -0
- package/src/tool/eval-tool.ts +20 -342
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,39 @@
|
|
|
12
12
|
|
|
13
13
|
### Removed
|
|
14
14
|
|
|
15
|
+
## [2026.8.1] - 2026-08-01
|
|
16
|
+
|
|
17
|
+
### Breaking Changes
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Preserve rich live and terminal `eval` details when peeking detached cells,
|
|
26
|
+
including code, title, output, phase, status events, tool-call summaries,
|
|
27
|
+
duration, and structured displays; cancellation now remains authoritative
|
|
28
|
+
over late completion races
|
|
29
|
+
([#603](https://github.com/code-yeongyu/senpi/pull/603)).
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
## [2026.7.31-2] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
### Breaking Changes
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Include a live elapsed label in detached `eval` footer status. The ticker updates only when the rendered duration
|
|
42
|
+
changes and is disposed when the cell completes, fails, or is stopped.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
### Removed
|
|
47
|
+
|
|
15
48
|
## [2026.7.31] - 2026-07-31
|
|
16
49
|
|
|
17
50
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -11,9 +11,12 @@ task-tool names are known.
|
|
|
11
11
|
- Persistent JavaScript, Python, Ruby, and Julia cells. State survives later
|
|
12
12
|
cells in the same language until reset, restart, or session disposal.
|
|
13
13
|
- Timeout detachment for interactive `eval`: long pure-compute cells return a
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
handle and continue in their existing kernel. Completion is injected with the
|
|
15
|
+
final value/error and buffered output; use `eval({ action: "peek"|"stop",
|
|
16
|
+
cell_id })` to inspect or terminate a detached cell. A running peek preserves
|
|
17
|
+
the original code and title together with current output, phase, status
|
|
18
|
+
events, tool-call summaries, elapsed duration, and structured display state;
|
|
19
|
+
a terminal peek preserves the exact final result.
|
|
17
20
|
- Loopback, bearer-authenticated kernel bridge with bounded JSONL frames.
|
|
18
21
|
- Structured status events for file operations, environment access, phases,
|
|
19
22
|
bridge activity, and delegated task progress.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-codemode",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.8.1",
|
|
4
4
|
"description": "Source-only senpi extension package for codemode evaluation tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@babel/parser": "8.0.4",
|
|
33
|
-
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.
|
|
33
|
+
"@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@^2026.8.1",
|
|
34
34
|
"typebox": "1.3.8"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@code-yeongyu/senpi": "*"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@code-yeongyu/senpi": "2026.
|
|
40
|
+
"@code-yeongyu/senpi": "2026.8.1"
|
|
41
41
|
},
|
|
42
42
|
"keywords": [
|
|
43
43
|
"senpi",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EvalDetachedCellStatusEntry } from "../tool/detached-cell-manager.ts";
|
|
2
|
+
import { formatEvalCellStatus } from "./eval-status.ts";
|
|
3
|
+
|
|
4
|
+
/** Footer live-elapsed refresh cadence while at least one detached cell is running. */
|
|
5
|
+
export const EVAL_STATUS_TICK_INTERVAL_MS = 1000;
|
|
6
|
+
|
|
7
|
+
/** Receives the freshly formatted footer status text (undefined clears the status). */
|
|
8
|
+
export type EvalStatusRender = (status: string | undefined) => void;
|
|
9
|
+
|
|
10
|
+
export interface EvalStatusTickerOptions {
|
|
11
|
+
readonly render: EvalStatusRender;
|
|
12
|
+
/** Injectable clock for tests; defaults to Date.now. */
|
|
13
|
+
readonly now?: () => number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Drives a once-per-second footer refresh while detached eval cells are running so
|
|
18
|
+
* the "↗ py · … (Ns)" elapsed label advances live instead of freezing between
|
|
19
|
+
* cell-set transitions. Same shape as the terminal builtin's MonitorStatusTicker:
|
|
20
|
+
* the interval is unref'd, and ticks producing the already-rendered label are skipped.
|
|
21
|
+
*/
|
|
22
|
+
export class EvalStatusTicker {
|
|
23
|
+
private readonly render: EvalStatusRender;
|
|
24
|
+
private readonly now: () => number;
|
|
25
|
+
private intervalId: NodeJS.Timeout | undefined;
|
|
26
|
+
private entries: readonly EvalDetachedCellStatusEntry[] = [];
|
|
27
|
+
private lastRenderedStatus: string | undefined;
|
|
28
|
+
private hasRendered = false;
|
|
29
|
+
|
|
30
|
+
constructor(options: EvalStatusTickerOptions) {
|
|
31
|
+
this.render = options.render;
|
|
32
|
+
this.now = options.now ?? Date.now;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get running(): boolean {
|
|
36
|
+
return this.intervalId !== undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Point the ticker at the current detached-cell set, render once immediately,
|
|
41
|
+
* and start the interval while cells are live (or stop it when none remain).
|
|
42
|
+
*/
|
|
43
|
+
sync(entries: readonly EvalDetachedCellStatusEntry[]): void {
|
|
44
|
+
this.entries = entries;
|
|
45
|
+
this.hasRendered = false;
|
|
46
|
+
this.tick();
|
|
47
|
+
if (entries.length === 0) {
|
|
48
|
+
this.stopInterval();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (this.intervalId !== undefined) return;
|
|
52
|
+
const handle = setInterval(() => this.tick(), EVAL_STATUS_TICK_INTERVAL_MS);
|
|
53
|
+
handle.unref();
|
|
54
|
+
this.intervalId = handle;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Stop the interval and drop the retained entries. */
|
|
58
|
+
stop(): void {
|
|
59
|
+
this.stopInterval();
|
|
60
|
+
this.entries = [];
|
|
61
|
+
this.lastRenderedStatus = undefined;
|
|
62
|
+
this.hasRendered = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private stopInterval(): void {
|
|
66
|
+
if (this.intervalId !== undefined) {
|
|
67
|
+
clearInterval(this.intervalId);
|
|
68
|
+
this.intervalId = undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private tick(): void {
|
|
73
|
+
const status = formatEvalCellStatus(this.entries, this.now());
|
|
74
|
+
if (this.hasRendered && status === this.lastRenderedStatus) return;
|
|
75
|
+
this.hasRendered = true;
|
|
76
|
+
this.lastRenderedStatus = status;
|
|
77
|
+
this.render(status);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -31,14 +31,47 @@ function labelOf(entry: EvalDetachedCellStatusEntry): string {
|
|
|
31
31
|
return entry.title === undefined || entry.title.length === 0 ? entry.cellId : entry.title;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Goal-style compact elapsed label (`5s`, `3m`, `2h 30m`, `1d 2h 3m`). Mirrors
|
|
36
|
+
* the monitor builtin's formatElapsedSeconds; kept local like the rest of the
|
|
37
|
+
* duplicated status-file helpers between the two packages.
|
|
38
|
+
*/
|
|
39
|
+
export function formatElapsedSeconds(value: number): string {
|
|
40
|
+
const seconds = Math.max(0, Math.trunc(value));
|
|
41
|
+
if (seconds < 60) return `${seconds}s`;
|
|
42
|
+
const minutes = Math.trunc(seconds / 60);
|
|
43
|
+
if (minutes < 60) return `${minutes}m`;
|
|
44
|
+
const hours = Math.trunc(minutes / 60);
|
|
45
|
+
const remainingMinutes = minutes % 60;
|
|
46
|
+
if (hours >= 24) {
|
|
47
|
+
const days = Math.trunc(hours / 24);
|
|
48
|
+
const remainingHours = hours % 24;
|
|
49
|
+
return `${days}d ${remainingHours}h ${remainingMinutes}m`;
|
|
50
|
+
}
|
|
51
|
+
if (remainingMinutes === 0) return `${hours}h`;
|
|
52
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Whole seconds since the oldest detached cell was created; never negative on clock skew. */
|
|
56
|
+
export function evalCellElapsedSeconds(entries: readonly EvalDetachedCellStatusEntry[], nowMs: number): number {
|
|
57
|
+
let oldest = Number.POSITIVE_INFINITY;
|
|
58
|
+
for (const entry of entries) oldest = Math.min(oldest, entry.startedAtMs);
|
|
59
|
+
if (!Number.isFinite(oldest)) return 0;
|
|
60
|
+
return Math.max(0, Math.round((nowMs - oldest) / 1000));
|
|
61
|
+
}
|
|
62
|
+
|
|
34
63
|
/** Brief footer text for the cells still running detached; undefined clears the status. */
|
|
35
|
-
export function formatEvalCellStatus(
|
|
64
|
+
export function formatEvalCellStatus(
|
|
65
|
+
entries: readonly EvalDetachedCellStatusEntry[],
|
|
66
|
+
nowMs: number,
|
|
67
|
+
): string | undefined {
|
|
36
68
|
const first = entries[0];
|
|
37
69
|
if (first === undefined) return undefined;
|
|
70
|
+
const suffix = ` (${formatElapsedSeconds(evalCellElapsedSeconds(entries, nowMs))})`;
|
|
38
71
|
if (entries.length === 1) {
|
|
39
72
|
const head = `${DETACHED_GLYPH} ${first.language} · `;
|
|
40
|
-
return head + truncateEnd(labelOf(first), MAX_STATUS_LENGTH - head.length);
|
|
73
|
+
return head + truncateEnd(labelOf(first), MAX_STATUS_LENGTH - head.length - suffix.length) + suffix;
|
|
41
74
|
}
|
|
42
75
|
const head = `${DETACHED_GLYPH} eval ${entries.length}: `;
|
|
43
|
-
return head + packLabels(entries.map(labelOf), MAX_STATUS_LENGTH - head.length);
|
|
76
|
+
return head + packLabels(entries.map(labelOf), MAX_STATUS_LENGTH - head.length - suffix.length) + suffix;
|
|
44
77
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type { EvalSchemaToolInfo } from "./bridges/schema-bridge.ts";
|
|
|
5
5
|
import { type CompletionRequest, type CompletionResult, createCompletionHandler } from "./completion/handler.ts";
|
|
6
6
|
import { defaultCodemodeSettings } from "./config/settings.ts";
|
|
7
7
|
import { EvalNotifier } from "./extension/eval-notifier.ts";
|
|
8
|
-
import { EVAL_CELLS_STATUS_KEY
|
|
8
|
+
import { EVAL_CELLS_STATUS_KEY } from "./extension/eval-status.ts";
|
|
9
|
+
import { EvalStatusTicker } from "./extension/eval-status-ticker.ts";
|
|
9
10
|
import {
|
|
10
11
|
createExecuteTool,
|
|
11
12
|
createRuntime,
|
|
@@ -44,6 +45,8 @@ export interface SenpiCodemodeOptions {
|
|
|
44
45
|
options: CreateCodemodeSessionManagerOptions,
|
|
45
46
|
) => CodemodeSessionManager | Promise<CodemodeSessionManager>;
|
|
46
47
|
readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
48
|
+
/** Injectable clock for detached-cell elapsed labels; defaults to Date.now. */
|
|
49
|
+
readonly now?: () => number;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCodemodeOptions = {}): void {
|
|
@@ -59,17 +62,22 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
59
62
|
getContext: () => activeContext,
|
|
60
63
|
getMode: () => "wake",
|
|
61
64
|
});
|
|
65
|
+
const statusTicker = new EvalStatusTicker({
|
|
66
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
67
|
+
render: (status) => {
|
|
68
|
+
const ctx = activeContext;
|
|
69
|
+
if (ctx?.ui?.setStatus === undefined) return;
|
|
70
|
+
const theme = ctx.ui.theme;
|
|
71
|
+
ctx.ui.setStatus(
|
|
72
|
+
EVAL_CELLS_STATUS_KEY,
|
|
73
|
+
status === undefined || ctx.mode !== "tui" || theme === undefined
|
|
74
|
+
? status
|
|
75
|
+
: theme.bg("selectedBg", theme.fg("text", status)),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
62
79
|
const showDetachedCells = (entries: readonly EvalDetachedCellStatusEntry[]): void => {
|
|
63
|
-
|
|
64
|
-
if (ctx?.ui?.setStatus === undefined) return;
|
|
65
|
-
const status = formatEvalCellStatus(entries);
|
|
66
|
-
const theme = ctx.ui.theme;
|
|
67
|
-
ctx.ui.setStatus(
|
|
68
|
-
EVAL_CELLS_STATUS_KEY,
|
|
69
|
-
status === undefined || ctx.mode !== "tui" || theme === undefined
|
|
70
|
-
? status
|
|
71
|
-
: theme.bg("selectedBg", theme.fg("text", status)),
|
|
72
|
-
);
|
|
80
|
+
statusTicker.sync(entries);
|
|
73
81
|
};
|
|
74
82
|
const registerEvalForRuntime = (
|
|
75
83
|
runtime: SessionRuntime,
|
|
@@ -101,6 +109,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
101
109
|
activeRuntime = undefined;
|
|
102
110
|
activeModelId = undefined;
|
|
103
111
|
activeCells = undefined;
|
|
112
|
+
statusTicker.stop();
|
|
104
113
|
await cells?.dispose();
|
|
105
114
|
activeContext = undefined;
|
|
106
115
|
await manager.dispose();
|
|
@@ -114,7 +123,11 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
114
123
|
listTools: () => pi.getAllTools(),
|
|
115
124
|
complete,
|
|
116
125
|
settings: defaultCodemodeSettings,
|
|
117
|
-
cellManager: new EvalDetachedCellManager({
|
|
126
|
+
cellManager: new EvalDetachedCellManager({
|
|
127
|
+
notifier,
|
|
128
|
+
onStatusChange: showDetachedCells,
|
|
129
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
130
|
+
}),
|
|
118
131
|
executionTracker: manager,
|
|
119
132
|
renderers,
|
|
120
133
|
hostLine: hostLine(),
|
|
@@ -143,6 +156,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
143
156
|
artifactsDir: runtime.artifactsDir,
|
|
144
157
|
notifier,
|
|
145
158
|
onStatusChange: showDetachedCells,
|
|
159
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
146
160
|
});
|
|
147
161
|
activeCells = cellManager;
|
|
148
162
|
activeRuntime = runtime;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
|
|
2
|
+
import type { EvalKernel } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
const INTERRUPT_DELIVERY_GRACE_MS = 100;
|
|
5
|
+
|
|
6
|
+
export interface EvalTimeoutFactory {
|
|
7
|
+
create(options: IdleTimeoutOptions): TimeoutPauseHandle & { dispose(): void };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const defaultTimeoutFactory: EvalTimeoutFactory = {
|
|
11
|
+
create(options): IdleTimeout {
|
|
12
|
+
return new IdleTimeout(options);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface CellExecutionOptions {
|
|
17
|
+
readonly callerSignal: AbortSignal;
|
|
18
|
+
readonly cellId: string;
|
|
19
|
+
readonly timeoutMs: number;
|
|
20
|
+
readonly timeoutFactory: EvalTimeoutFactory;
|
|
21
|
+
readonly onTimeout: (error: Error) => void;
|
|
22
|
+
readonly onAbort: (error: Error) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CellExecution {
|
|
26
|
+
readonly #callerSignal: AbortSignal;
|
|
27
|
+
readonly #onAbort: (error: Error) => void;
|
|
28
|
+
readonly #abortPromise: Promise<never>;
|
|
29
|
+
readonly #detachedPromise: Promise<void>;
|
|
30
|
+
readonly #watchdog: TimeoutPauseHandle & { dispose(): void };
|
|
31
|
+
#rejectAbort: ((reason?: unknown) => void) | undefined;
|
|
32
|
+
#resolveDetached: (() => void) | undefined;
|
|
33
|
+
#kernel: EvalKernel | undefined;
|
|
34
|
+
#interruptDeadline: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
#active = true;
|
|
36
|
+
|
|
37
|
+
constructor(options: CellExecutionOptions) {
|
|
38
|
+
this.#callerSignal = options.callerSignal;
|
|
39
|
+
this.#onAbort = options.onAbort;
|
|
40
|
+
this.#abortPromise = new Promise<never>((_resolve, reject) => {
|
|
41
|
+
this.#rejectAbort = reject;
|
|
42
|
+
});
|
|
43
|
+
this.#detachedPromise = new Promise<void>((resolve) => {
|
|
44
|
+
this.#resolveDetached = resolve;
|
|
45
|
+
});
|
|
46
|
+
this.#watchdog = options.timeoutFactory.create({
|
|
47
|
+
cellId: options.cellId,
|
|
48
|
+
timeoutMs: options.timeoutMs,
|
|
49
|
+
onTimeout: ({ error }) => options.onTimeout(error),
|
|
50
|
+
});
|
|
51
|
+
this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, {
|
|
52
|
+
once: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get detached(): Promise<void> {
|
|
57
|
+
return this.#detachedPromise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pause(): void {
|
|
61
|
+
this.#watchdog.pause();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
resume(): void {
|
|
65
|
+
this.#watchdog.resume();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setKernel(kernel: EvalKernel): void {
|
|
69
|
+
this.#kernel = kernel;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
detach(): void {
|
|
73
|
+
if (!this.#active) return;
|
|
74
|
+
this.#watchdog.dispose();
|
|
75
|
+
this.#resolveDetached?.();
|
|
76
|
+
this.#resolveDetached = undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
cancel(reason: unknown): void {
|
|
80
|
+
this.#abort(reason);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
finish(): void {
|
|
84
|
+
this.#active = false;
|
|
85
|
+
this.#cleanup();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async wait<Result>(operation: Promise<Result>): Promise<Result> {
|
|
89
|
+
const guarded = operation.then(
|
|
90
|
+
(value): Result | Promise<never> => (this.#active ? value : this.#abortPromise),
|
|
91
|
+
(reason: unknown): Promise<never> => (this.#active ? Promise.reject(reason) : this.#abortPromise),
|
|
92
|
+
);
|
|
93
|
+
return await Promise.race([guarded, this.#abortPromise]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
readonly #handleCallerAbort = (): void => {
|
|
97
|
+
this.#abort(this.#callerSignal.reason);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
interruptStateRetained: Promise<boolean> | undefined;
|
|
101
|
+
|
|
102
|
+
#abort(reason: unknown): void {
|
|
103
|
+
if (!this.#active) return;
|
|
104
|
+
this.#active = false;
|
|
105
|
+
this.#cleanup();
|
|
106
|
+
const error = abortError(reason);
|
|
107
|
+
this.#onAbort(error);
|
|
108
|
+
const kernel = this.#kernel;
|
|
109
|
+
if (kernel === undefined) {
|
|
110
|
+
this.#settleAbort(error);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
|
|
114
|
+
void Promise.resolve()
|
|
115
|
+
.then(async () => {
|
|
116
|
+
const handle = await kernel.interrupt(error.message);
|
|
117
|
+
this.interruptStateRetained = handle?.stateRetained;
|
|
118
|
+
})
|
|
119
|
+
.then(
|
|
120
|
+
() => this.#settleAbort(error),
|
|
121
|
+
(interruptError: unknown) => this.#settleAbort(interruptError),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#settleAbort(reason: unknown): void {
|
|
126
|
+
const reject = this.#rejectAbort;
|
|
127
|
+
if (reject === undefined) return;
|
|
128
|
+
this.#rejectAbort = undefined;
|
|
129
|
+
if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
|
|
130
|
+
reject(reason);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#cleanup(): void {
|
|
134
|
+
this.#callerSignal.removeEventListener("abort", this.#handleCallerAbort);
|
|
135
|
+
this.#watchdog.dispose();
|
|
136
|
+
if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
|
|
137
|
+
this.#interruptDeadline = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function abortError(reason: unknown): Error {
|
|
142
|
+
if (reason instanceof Error && reason.name !== "AbortError") return reason;
|
|
143
|
+
const error = new Error(typeof reason === "string" ? reason : "Eval interrupted", { cause: reason });
|
|
144
|
+
error.name = "AbortError";
|
|
145
|
+
return error;
|
|
146
|
+
}
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AgentToolResult,
|
|
3
|
-
type AgentToolUpdateCallback,
|
|
4
|
-
type ExtensionContext,
|
|
5
|
-
sanitizeTerminalLabel,
|
|
6
|
-
} from "@code-yeongyu/senpi";
|
|
1
|
+
import { type AgentToolResult, type ExtensionContext, sanitizeTerminalLabel } from "@code-yeongyu/senpi";
|
|
7
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
8
3
|
import type { AgentExecuteTool } from "../bridges/agent-bridge.ts";
|
|
9
4
|
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
@@ -13,15 +8,12 @@ import type { CompletionRequest, CompletionResult } from "../completion/handler.
|
|
|
13
8
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
14
9
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
15
10
|
import { boundToolCallArgs, capCodePoints, MAX_ENRICHED_TOOL_CALLS, toolCallResultPreview } from "./call-capture.ts";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
EvalOutputCollector,
|
|
19
|
-
type EvalOutputResult,
|
|
20
|
-
marshalToolResult,
|
|
21
|
-
toolResultIsError,
|
|
22
|
-
} from "./image.ts";
|
|
11
|
+
import { CellResultBuilder, type CellState } from "./cell-runtime.ts";
|
|
12
|
+
import { type EvalImageResizer, marshalToolResult, toolResultIsError } from "./image.ts";
|
|
23
13
|
import { upsertStatusEvent } from "./status-events.ts";
|
|
24
|
-
import type { EvalKernel, EvalStatusEvent, EvalToolDetails
|
|
14
|
+
import type { EvalKernel, EvalStatusEvent, EvalToolDetails } from "./types.ts";
|
|
15
|
+
|
|
16
|
+
export type { CellState } from "./cell-runtime.ts";
|
|
25
17
|
|
|
26
18
|
type ResolvedToolReply = {
|
|
27
19
|
readonly value: unknown;
|
|
@@ -37,22 +29,6 @@ type ToolCallEnrichment = {
|
|
|
37
29
|
readonly argsTruncated?: true;
|
|
38
30
|
};
|
|
39
31
|
|
|
40
|
-
const LIVE_OUTPUT_PREVIEW_LINES = 8;
|
|
41
|
-
|
|
42
|
-
export interface CellState {
|
|
43
|
-
readonly input: EvalToolInput;
|
|
44
|
-
readonly signal: AbortSignal;
|
|
45
|
-
readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
|
|
46
|
-
readonly toolCalls: EvalToolDetails["toolCalls"] extends readonly (infer Item)[] ? Item[] : never;
|
|
47
|
-
readonly pendingBridgeCalls: Promise<void>[];
|
|
48
|
-
readonly statusEvents: EvalStatusEvent[];
|
|
49
|
-
active: boolean;
|
|
50
|
-
output: string;
|
|
51
|
-
phase: string | undefined;
|
|
52
|
-
durationMs: number;
|
|
53
|
-
status: "pending" | "running" | "complete" | "error";
|
|
54
|
-
}
|
|
55
|
-
|
|
56
32
|
export interface CellBridgeRuntime {
|
|
57
33
|
readonly executeTool: AgentExecuteTool;
|
|
58
34
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
@@ -67,46 +43,40 @@ export class CellHandler {
|
|
|
67
43
|
readonly #kernel: EvalKernel;
|
|
68
44
|
readonly #state: CellState;
|
|
69
45
|
readonly #runtime: CellBridgeRuntime;
|
|
70
|
-
readonly #
|
|
46
|
+
readonly #resultBuilder: CellResultBuilder;
|
|
71
47
|
|
|
72
48
|
constructor(kernel: EvalKernel, state: CellState, runtime: CellBridgeRuntime) {
|
|
73
49
|
this.#kernel = kernel;
|
|
74
50
|
this.#state = state;
|
|
75
51
|
this.#runtime = runtime;
|
|
76
52
|
const settings = runtime.settings.outputSink;
|
|
77
|
-
this.#
|
|
53
|
+
this.#resultBuilder = new CellResultBuilder({
|
|
54
|
+
state,
|
|
78
55
|
headBytes: settings.headBytes,
|
|
79
56
|
maxColumns: settings.maxColumns,
|
|
80
57
|
model: runtime.ctx.model,
|
|
81
58
|
...(runtime.artifactPath === undefined ? {} : { artifactPath: runtime.artifactPath }),
|
|
82
59
|
...(runtime.imageResizer === undefined ? {} : { imageResizer: runtime.imageResizer }),
|
|
83
|
-
onChunk: (_aggregate, cell) => {
|
|
84
|
-
state.output = cell;
|
|
85
|
-
this.#emitUpdate(false);
|
|
86
|
-
},
|
|
87
60
|
});
|
|
88
|
-
state.status = "running";
|
|
89
|
-
this.#emitUpdate(false);
|
|
90
61
|
}
|
|
91
62
|
|
|
92
63
|
async handle(message: KernelToHostMessage): Promise<void> {
|
|
93
64
|
if (!this.#state.active) return;
|
|
94
65
|
switch (message.type) {
|
|
95
66
|
case "text":
|
|
96
|
-
this.#
|
|
67
|
+
this.#resultBuilder.push(message.data);
|
|
97
68
|
return;
|
|
98
69
|
case "phase":
|
|
99
|
-
this.#
|
|
100
|
-
this.#emitUpdate(false);
|
|
70
|
+
this.#resultBuilder.setPhase(message.title);
|
|
101
71
|
return;
|
|
102
72
|
case "status":
|
|
103
73
|
this.#recordStatus(message.event);
|
|
104
74
|
return;
|
|
105
75
|
case "log":
|
|
106
|
-
this.#
|
|
76
|
+
this.#resultBuilder.push(`${message.message}\n`);
|
|
107
77
|
return;
|
|
108
78
|
case "display":
|
|
109
|
-
this.#
|
|
79
|
+
this.#resultBuilder.display(message);
|
|
110
80
|
return;
|
|
111
81
|
case "tool-call": {
|
|
112
82
|
const pending = this.#handleToolCall(message);
|
|
@@ -125,38 +95,19 @@ export class CellHandler {
|
|
|
125
95
|
}
|
|
126
96
|
|
|
127
97
|
async finalize(result: Extract<KernelToHostMessage, { type: "result" }>): Promise<AgentToolResult<EvalToolDetails>> {
|
|
128
|
-
this.#
|
|
129
|
-
if (result.ok) {
|
|
130
|
-
if (result.valueRepr) this.#output.push(`${result.valueRepr}\n`);
|
|
131
|
-
this.#state.status = "complete";
|
|
132
|
-
} else {
|
|
133
|
-
this.#output.push(`${result.error.message}\n`);
|
|
134
|
-
this.#state.status = "error";
|
|
135
|
-
}
|
|
136
|
-
return await this.#finish(!result.ok);
|
|
98
|
+
return await this.#resultBuilder.finalize(result);
|
|
137
99
|
}
|
|
138
100
|
|
|
139
101
|
async finalizeCancellation(error: Error): Promise<AgentToolResult<EvalToolDetails>> {
|
|
140
|
-
this.#
|
|
141
|
-
this.#state.status = "error";
|
|
142
|
-
return await this.#finish(true);
|
|
102
|
+
return await this.#resultBuilder.finalizeCancellation(error);
|
|
143
103
|
}
|
|
144
104
|
|
|
145
105
|
async flushOutput(): Promise<void> {
|
|
146
|
-
await this.#
|
|
106
|
+
await this.#resultBuilder.flushOutput();
|
|
147
107
|
}
|
|
148
108
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.#state.output = output.output;
|
|
152
|
-
const details = this.#details(output, isError);
|
|
153
|
-
this.#emitUpdate(isError);
|
|
154
|
-
const text =
|
|
155
|
-
output.output ||
|
|
156
|
-
(output.images.length > 0
|
|
157
|
-
? `(displayed ${output.images.length} image${output.images.length === 1 ? "" : "s"}; no text output)`
|
|
158
|
-
: "(no output)");
|
|
159
|
-
return { content: [{ type: "text", text }, ...output.images], details };
|
|
109
|
+
liveResult(): AgentToolResult<EvalToolDetails> {
|
|
110
|
+
return this.#resultBuilder.liveResult();
|
|
160
111
|
}
|
|
161
112
|
|
|
162
113
|
async #handleToolCall(message: Extract<KernelToHostMessage, { type: "tool-call" }>): Promise<void> {
|
|
@@ -202,7 +153,7 @@ export class CellHandler {
|
|
|
202
153
|
? { name: message.toolName, ok: true }
|
|
203
154
|
: { name: message.toolName, ok: false, error: result.error },
|
|
204
155
|
);
|
|
205
|
-
this.#emitUpdate(false);
|
|
156
|
+
this.#resultBuilder.emitUpdate(false);
|
|
206
157
|
return;
|
|
207
158
|
}
|
|
208
159
|
const capturedArgs = boundToolCallArgs(message.args);
|
|
@@ -268,7 +219,7 @@ export class CellHandler {
|
|
|
268
219
|
error: { message: text },
|
|
269
220
|
});
|
|
270
221
|
}
|
|
271
|
-
this.#emitUpdate(false);
|
|
222
|
+
this.#resultBuilder.emitUpdate(false);
|
|
272
223
|
}
|
|
273
224
|
|
|
274
225
|
#pushToolCall(
|
|
@@ -301,55 +252,6 @@ export class CellHandler {
|
|
|
301
252
|
#recordStatus(event: EvalStatusEvent): void {
|
|
302
253
|
if (!this.#runtime.settings.statusEvents) return;
|
|
303
254
|
upsertStatusEvent(this.#state.statusEvents, event);
|
|
304
|
-
this.#emitUpdate(false);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
#details(output: EvalOutputResult | undefined, isError: boolean): EvalToolDetails {
|
|
308
|
-
const statusEvents = this.#state.statusEvents.length > 0 ? [...this.#state.statusEvents] : undefined;
|
|
309
|
-
return {
|
|
310
|
-
language: this.#state.input.language,
|
|
311
|
-
languages: [this.#state.input.language],
|
|
312
|
-
...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
|
|
313
|
-
durationMs: this.#state.durationMs,
|
|
314
|
-
toolCalls: [...this.#state.toolCalls],
|
|
315
|
-
truncated: output?.truncated ?? false,
|
|
316
|
-
...(isError ? { isError: true } : {}),
|
|
317
|
-
...(this.#state.phase === undefined ? {} : { phase: this.#state.phase }),
|
|
318
|
-
cells: [
|
|
319
|
-
{
|
|
320
|
-
index: 0,
|
|
321
|
-
...(this.#state.input.title === undefined ? {} : { title: this.#state.input.title }),
|
|
322
|
-
code: this.#state.input.code,
|
|
323
|
-
language: this.#state.input.language,
|
|
324
|
-
output: this.#state.output,
|
|
325
|
-
status: this.#state.status,
|
|
326
|
-
durationMs: this.#state.durationMs,
|
|
327
|
-
...(statusEvents === undefined ? {} : { statusEvents }),
|
|
328
|
-
...(output?.hasMarkdown ? { hasMarkdown: true } : {}),
|
|
329
|
-
},
|
|
330
|
-
],
|
|
331
|
-
...(statusEvents === undefined ? {} : { statusEvents }),
|
|
332
|
-
...(output === undefined || output.jsonOutputs.length === 0 ? {} : { jsonOutputs: output.jsonOutputs }),
|
|
333
|
-
...(output?.notice === undefined ? {} : { notice: output.notice }),
|
|
334
|
-
...(output?.meta === undefined ? {} : { meta: output.meta }),
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
#liveUpdateText(): string {
|
|
339
|
-
const title = this.#state.input.title === undefined ? "" : ` ${this.#state.input.title}`;
|
|
340
|
-
const aggregateOutput = this.#output.aggregateText();
|
|
341
|
-
const outputLines = aggregateOutput.split("\n");
|
|
342
|
-
const hasTrailingNewline = aggregateOutput.endsWith("\n");
|
|
343
|
-
if (hasTrailingNewline) outputLines.pop();
|
|
344
|
-
const output = `${outputLines.slice(-LIVE_OUTPUT_PREVIEW_LINES).join("\n")}${hasTrailingNewline ? "\n" : ""}`;
|
|
345
|
-
return `1/1 cells ${this.#state.status}\n[1] ${this.#state.input.language}${title} ${this.#state.status}${output.length === 0 ? "" : `\n${output}`}`;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
#emitUpdate(isError: boolean): void {
|
|
349
|
-
if (!this.#state.active) return;
|
|
350
|
-
this.#state.onUpdate?.({
|
|
351
|
-
content: [{ type: "text", text: this.#liveUpdateText() }],
|
|
352
|
-
details: this.#details(undefined, isError),
|
|
353
|
-
});
|
|
255
|
+
this.#resultBuilder.emitUpdate(false);
|
|
354
256
|
}
|
|
355
257
|
}
|