@code-yeongyu/senpi-codemode 2026.7.31 → 2026.8.3

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.
@@ -1,206 +1,22 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { join } from "node:path";
3
- import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
4
- import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
5
- import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
6
- import { defaultCodemodeSettings, type ResolvedCodemodeSettings } from "../config/settings.ts";
7
- import type { EvalExecutionTracker } from "../extension/session-manager.ts";
3
+ import type { AgentToolResult, ExtensionContext, ToolDefinition } from "@code-yeongyu/senpi";
4
+ import { defaultCodemodeSettings } from "../config/settings.ts";
8
5
  import { buildEvalPrompt } from "../prompt/eval-prompt.ts";
9
6
  import { TIMEOUT_PAUSE_OP, TIMEOUT_RESUME_OP } from "../timeouts/bridge-timeout.ts";
10
- import { IdleTimeout, type IdleTimeoutOptions, type TimeoutPauseHandle } from "../timeouts/idle-timeout.ts";
7
+ import { abortError, CellExecution, defaultTimeoutFactory } from "./cell-execution.ts";
11
8
  import { CellHandler, type CellState } from "./cell-handler.ts";
12
- import { EvalDetachedCellManager, type EvalDetachedCellSnapshot } from "./detached-cell-manager.ts";
13
- import type { EvalImageResizer } from "./image.ts";
14
- import { describeTimeoutState, interruptionStateNote } from "./interrupt-note.ts";
15
- import {
16
- createEvalInputSchema,
17
- type EnabledEvalLanguages,
18
- type EvalCellResult,
19
- type EvalControlInput,
20
- type EvalInputSchema,
21
- type EvalKernel,
22
- type EvalKernelManager,
23
- type EvalToolDetails,
24
- type EvalToolInput,
25
- type EvalToolRequest,
26
- type ExecuteTool,
27
- enabledLanguageList,
28
- } from "./types.ts";
29
-
9
+ import { EvalDetachedCellManager } from "./detached-cell-manager.ts";
10
+ import { detachedKernelBusyError, executeEvalControl, resultAfterDetach } from "./detached-eval-result.ts";
11
+ import { evalTimeoutBehavior, isEvalControlRequest, parseEvalRequest } from "./eval-request.ts";
12
+ import type { CreateEvalToolOptions, EvalCellInvocation } from "./eval-tool-options.ts";
13
+ import { describeTimeoutState } from "./interrupt-note.ts";
14
+ import { createEvalInputSchema, type EvalInputSchema, type EvalToolDetails, enabledLanguageList } from "./types.ts";
15
+
16
+ export type { EvalTimeoutFactory } from "./cell-execution.ts";
17
+ export type { CreateEvalToolOptions } from "./eval-tool-options.ts";
30
18
  export type { EnabledEvalLanguages, EvalKernel, EvalKernelManager } from "./types.ts";
31
19
 
32
- export interface EvalTimeoutFactory {
33
- create(options: IdleTimeoutOptions): TimeoutPauseHandle & { dispose(): void };
34
- }
35
-
36
- export interface CreateEvalToolOptions {
37
- readonly enabledLanguages: EnabledEvalLanguages;
38
- readonly kernelManager: EvalKernelManager;
39
- readonly cellTimeoutSeconds: number;
40
- readonly executeTool: ExecuteTool;
41
- readonly listTools?: () => readonly EvalSchemaToolInfo[];
42
- readonly complete?: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
43
- readonly settings?: ResolvedCodemodeSettings;
44
- readonly artifactsDir?: string;
45
- readonly imageResizer?: EvalImageResizer;
46
- readonly executionTracker?: EvalExecutionTracker;
47
- readonly cellManager?: EvalDetachedCellManager;
48
- readonly timeoutFactory?: EvalTimeoutFactory;
49
- readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
50
- readonly renderers?: Pick<ToolDefinition<EvalInputSchema, EvalToolDetails>, "renderCall" | "renderResult">;
51
- /** Whether the task-tool spawn helpers (agent()/output()/<dag>) are advertised in the prompt. */
52
- readonly spawns?: boolean;
53
- /** Default agent name surfaced in the agent() helper docs when spawns are enabled. */
54
- readonly spawnDefaultAgent?: string;
55
- /** Active model id; selects the emphasis dialect of the eval prompt. */
56
- readonly modelId?: string;
57
- /** Preformatted host line rendered into the prompt's host-sizing note. */
58
- readonly hostLine?: string;
59
- }
60
-
61
- interface EvalCellInvocation {
62
- readonly cellId: string;
63
- readonly input: EvalToolInput;
64
- readonly signal: AbortSignal;
65
- readonly onUpdate: AgentToolUpdateCallback<EvalToolDetails> | undefined;
66
- readonly ctx: ExtensionContext;
67
- }
68
-
69
- interface CellExecutionOptions {
70
- readonly callerSignal: AbortSignal;
71
- readonly cellId: string;
72
- readonly timeoutMs: number;
73
- readonly timeoutFactory: EvalTimeoutFactory;
74
- readonly onTimeout: (error: Error) => void;
75
- readonly onAbort: (error: Error) => void;
76
- }
77
-
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
- };
86
-
87
- class CellExecution {
88
- readonly #callerSignal: AbortSignal;
89
- readonly #onAbort: (error: Error) => void;
90
- readonly #abortPromise: Promise<never>;
91
- readonly #detachedPromise: Promise<void>;
92
- readonly #watchdog: TimeoutPauseHandle & { dispose(): void };
93
- #rejectAbort: ((reason?: unknown) => void) | undefined;
94
- #resolveDetached: (() => void) | undefined;
95
- #kernel: EvalKernel | undefined;
96
- #interruptDeadline: ReturnType<typeof setTimeout> | undefined;
97
- #active = true;
98
-
99
- constructor(options: CellExecutionOptions) {
100
- this.#callerSignal = options.callerSignal;
101
- this.#onAbort = options.onAbort;
102
- this.#abortPromise = new Promise<never>((_resolve, reject) => {
103
- this.#rejectAbort = reject;
104
- });
105
- this.#detachedPromise = new Promise<void>((resolve) => {
106
- this.#resolveDetached = resolve;
107
- });
108
- this.#watchdog = options.timeoutFactory.create({
109
- cellId: options.cellId,
110
- timeoutMs: options.timeoutMs,
111
- onTimeout: ({ error }) => options.onTimeout(error),
112
- });
113
- this.#callerSignal.addEventListener("abort", this.#handleCallerAbort, { once: true });
114
- }
115
-
116
- get detached(): Promise<void> {
117
- return this.#detachedPromise;
118
- }
119
-
120
- pause(): void {
121
- this.#watchdog.pause();
122
- }
123
-
124
- resume(): void {
125
- this.#watchdog.resume();
126
- }
127
-
128
- setKernel(kernel: EvalKernel): void {
129
- this.#kernel = kernel;
130
- }
131
-
132
- detach(): void {
133
- if (!this.#active) return;
134
- this.#watchdog.dispose();
135
- this.#resolveDetached?.();
136
- this.#resolveDetached = undefined;
137
- }
138
-
139
- cancel(reason: unknown): void {
140
- this.#abort(reason);
141
- }
142
-
143
- finish(): void {
144
- this.#active = false;
145
- this.#cleanup();
146
- }
147
-
148
- async wait<Result>(operation: Promise<Result>): Promise<Result> {
149
- const guarded = operation.then(
150
- (value): Result | Promise<never> => (this.#active ? value : this.#abortPromise),
151
- (reason: unknown): Promise<never> => (this.#active ? Promise.reject(reason) : this.#abortPromise),
152
- );
153
- return await Promise.race([guarded, this.#abortPromise]);
154
- }
155
-
156
- readonly #handleCallerAbort = (): void => {
157
- this.#abort(this.#callerSignal.reason);
158
- };
159
-
160
- /** Outcome of the most recent interrupt, when a kernel was interrupted. */
161
- interruptStateRetained: Promise<boolean> | undefined;
162
-
163
- #abort(reason: unknown): void {
164
- if (!this.#active) return;
165
- this.#active = false;
166
- this.#cleanup();
167
- const error = abortError(reason);
168
- this.#onAbort(error);
169
- const kernel = this.#kernel;
170
- if (kernel === undefined) {
171
- this.#settleAbort(error);
172
- return;
173
- }
174
- this.#interruptDeadline = setTimeout(() => this.#settleAbort(error), INTERRUPT_DELIVERY_GRACE_MS);
175
- void Promise.resolve()
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
- })
182
- .then(
183
- () => this.#settleAbort(error),
184
- (interruptError: unknown) => this.#settleAbort(interruptError),
185
- );
186
- }
187
-
188
- #settleAbort(reason: unknown): void {
189
- const reject = this.#rejectAbort;
190
- if (reject === undefined) return;
191
- this.#rejectAbort = undefined;
192
- if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
193
- reject(reason);
194
- }
195
-
196
- #cleanup(): void {
197
- this.#callerSignal.removeEventListener("abort", this.#handleCallerAbort);
198
- this.#watchdog.dispose();
199
- if (this.#interruptDeadline !== undefined) clearTimeout(this.#interruptDeadline);
200
- this.#interruptDeadline = undefined;
201
- }
202
- }
203
-
204
20
  export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<EvalInputSchema, EvalToolDetails> {
205
21
  const parameters = createEvalInputSchema(options.enabledLanguages);
206
22
  const prompt = buildEvalPrompt(options.enabledLanguages, {
@@ -222,15 +38,15 @@ export function createEvalTool(options: CreateEvalToolOptions): ToolDefinition<E
222
38
  ...(options.renderers?.renderCall === undefined ? {} : { renderCall: options.renderers.renderCall }),
223
39
  ...(options.renderers?.renderResult === undefined ? {} : { renderResult: options.renderers.renderResult }),
224
40
  async execute(toolCallId, params, signal, onUpdate, ctx) {
225
- const request = requestFrom(params);
226
- if (isControlRequest(request)) return await executeControl(cellManager, request);
41
+ const request = parseEvalRequest(params);
42
+ if (isEvalControlRequest(request)) return await executeEvalControl(cellManager, request);
227
43
  if (options.proxyExecutor) return await options.proxyExecutor(request, signal);
228
44
  if (!languages.includes(request.language))
229
45
  throw new RangeError(
230
46
  `Unsupported eval language "${request.language}". Enabled languages: ${languages.join(", ")}`,
231
47
  );
232
48
  const busy = cellManager.busyFor(request.language);
233
- if (busy !== undefined) throw kernelBusyError(busy);
49
+ if (busy !== undefined) throw detachedKernelBusyError(busy);
234
50
  options.executionTracker?.assertEvalExecutionAllowed();
235
51
  const lifecycleController = new AbortController();
236
52
  const combinedSignal = signal
@@ -257,7 +73,7 @@ async function runEvalCell(
257
73
  ): Promise<AgentToolResult<EvalToolDetails>> {
258
74
  if (invocation.signal.aborted) throw abortError(invocation.signal.reason);
259
75
  const timeoutMs = Math.floor((invocation.input.timeout ?? options.cellTimeoutSeconds) * 1_000);
260
- const timeoutBehavior = timeoutBehaviorFor(invocation.input, invocation.ctx);
76
+ const timeoutBehavior = evalTimeoutBehavior(invocation.input, invocation.ctx);
261
77
  const bridgeAbortController = new AbortController();
262
78
  const cellSignal = AbortSignal.any([invocation.signal, bridgeAbortController.signal]);
263
79
  const bridgeContext: ExtensionContext = { ...invocation.ctx, signal: cellSignal };
@@ -317,7 +133,7 @@ async function runEvalCell(
317
133
  finalized.then((result) => ({ kind: "result" as const, result })),
318
134
  execution.detached.then(() => ({ kind: "detached" as const })),
319
135
  ]);
320
- if (outcome.kind === "detached") return detachedResult(cellManager.peek(invocation.cellId), invocation.input);
136
+ if (outcome.kind === "detached") return resultAfterDetach(cellManager.peek(invocation.cellId), invocation.input);
321
137
  return outcome.result;
322
138
  }
323
139
 
@@ -351,7 +167,7 @@ async function executeCell(
351
167
  }),
352
168
  );
353
169
  execution.setKernel(kernel);
354
- handler = new CellHandler(kernel, state, {
170
+ const activeHandler = new CellHandler(kernel, state, {
355
171
  executeTool: options.executeTool,
356
172
  ...(options.listTools === undefined ? {} : { listTools: options.listTools }),
357
173
  settings: options.settings ?? defaultCodemodeSettings,
@@ -362,7 +178,8 @@ async function executeCell(
362
178
  : { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
363
179
  ...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
364
180
  });
365
- cellManager.markRunning(cell, kernel, () => state.output);
181
+ handler = activeHandler;
182
+ cellManager.markRunning(cell, kernel, () => activeHandler.liveResult());
366
183
  if ("setContext" in options.kernelManager && typeof options.kernelManager.setContext === "function") {
367
184
  options.kernelManager.setContext(bridgeContext);
368
185
  }
@@ -382,142 +199,3 @@ async function executeCell(
382
199
  if (handler) await handler.flushOutput();
383
200
  }
384
201
  }
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
-
518
- function abortError(reason: unknown): Error {
519
- if (reason instanceof Error && reason.name !== "AbortError") return reason;
520
- const error = new Error(typeof reason === "string" ? reason : "Eval interrupted", { cause: reason });
521
- error.name = "AbortError";
522
- return error;
523
- }