@code-yeongyu/senpi-codemode 2026.10.1 → 2026.10.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.
- package/CHANGELOG.md +66 -0
- package/README.md +121 -4
- package/package.json +5 -4
- package/src/bridge/http-server.ts +1 -0
- package/src/extension/eval-status-ticker.ts +20 -5
- package/src/extension/runtime-factory.ts +13 -2
- package/src/extension/session-cwd.ts +35 -0
- package/src/extension/session-manager.ts +11 -1
- package/src/extension/stale-context.ts +17 -0
- package/src/kernels/AGENTS.md +4 -0
- package/src/kernels/js/context-manager.ts +6 -7
- package/src/kernels/js/interrupt-bounds.ts +33 -1
- package/src/kernels/js/run-queue.ts +10 -1
- package/src/kernels/js/worker-core.js +11 -1
- package/src/kernels/js/worker-cwd.js +185 -0
- package/src/kernels/js/worker-host.ts +11 -6
- package/src/kernels/js/worker-runtime.js +13 -0
- package/src/kernels/js/worker-shell-capture.d.ts +2 -0
- package/src/kernels/js/worker-shell-capture.js +16 -3
- package/src/kernels/py/kernel-contract.ts +4 -0
- package/src/kernels/py/kernel.ts +3 -2
- package/src/kernels/py/prelude.py +11 -1
- package/src/kernels/py/process.ts +3 -1
- package/src/kernels/py/startup.ts +75 -0
- package/src/kernels/py/transport.ts +12 -11
- package/src/kernels/shared/subprocess-process.ts +3 -1
- package/src/prompt/eval-prompt-template.ts +1 -1
- package/src/skill/bun-1-4/SKILL.md +1 -1
- package/src/tool/cell-handler.ts +10 -14
- package/src/tool/image.ts +29 -2
- package/src/tool/render.ts +8 -5
- package/src/tool/run-eval-cell.ts +2 -2
- package/src/tool/types.ts +13 -5
package/src/tool/image.ts
CHANGED
|
@@ -30,6 +30,28 @@ const MAX_DISPLAY_IMAGES_PER_CELL = 8;
|
|
|
30
30
|
const MAX_DISPLAY_IMAGE_BYTES_PER_CELL = 24 * 1024 * 1024;
|
|
31
31
|
const MAX_JSON_OUTPUTS_PER_CELL = 64;
|
|
32
32
|
|
|
33
|
+
// Base64 of the signatures of the formats providers accept inline (PNG, JPEG except JPEG-LS, GIF,
|
|
34
|
+
// "RIFF....WEBP"). Signatures start at byte 0, so their encodings are prefixes.
|
|
35
|
+
const IMAGE_SIGNATURES: ReadonlyArray<readonly [string, RegExp]> = [
|
|
36
|
+
["image/png", /^iVBORw0KGg/],
|
|
37
|
+
["image/jpeg", /^[/]9j[/](?!9)/],
|
|
38
|
+
["image/gif", /^R0lGOD[dl]h/],
|
|
39
|
+
["image/webp", /^UklG.{8}RUJQ/],
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Providers reject the whole request on a bad image, and a kept image block is resent on every later turn,
|
|
43
|
+
// so invalid data is dropped with a reason instead. The detected type wins over the declared one.
|
|
44
|
+
function validateDisplayImage(dataBase64: string): { data: string; mimeType: string } | { reason: string } {
|
|
45
|
+
const data = dataBase64.replace(/\s+/g, "");
|
|
46
|
+
if (data.length === 0 || data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
|
|
47
|
+
return { reason: "the image data is not valid base64 (truncated or corrupted?)" };
|
|
48
|
+
}
|
|
49
|
+
const head = data.slice(0, 16);
|
|
50
|
+
const signature = IMAGE_SIGNATURES.find(([, pattern]) => pattern.test(head));
|
|
51
|
+
if (signature === undefined) return { reason: "the image data is not a PNG, JPEG, GIF, or WebP image" };
|
|
52
|
+
return { data, mimeType: signature[0] };
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
export interface EvalOutputOptions {
|
|
34
56
|
readonly artifactPath?: string;
|
|
35
57
|
readonly headBytes: number;
|
|
@@ -98,6 +120,11 @@ export class EvalOutputCollector {
|
|
|
98
120
|
|
|
99
121
|
display(message: DisplayMessage): void {
|
|
100
122
|
if (message.mimeType.startsWith("image/")) {
|
|
123
|
+
const image = validateDisplayImage(message.dataBase64);
|
|
124
|
+
if ("reason" in image) {
|
|
125
|
+
this.#sink.push(`[display: image dropped \u2014 ${image.reason}]\n`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
101
128
|
if (
|
|
102
129
|
this.#displayImages.length >= MAX_DISPLAY_IMAGES_PER_CELL ||
|
|
103
130
|
this.#displayImageBytes + message.dataBase64.length > MAX_DISPLAY_IMAGE_BYTES_PER_CELL
|
|
@@ -105,8 +132,8 @@ export class EvalOutputCollector {
|
|
|
105
132
|
this.#displayImagesElided++;
|
|
106
133
|
return;
|
|
107
134
|
}
|
|
108
|
-
this.#displayImages.push({ type: "image", mimeType:
|
|
109
|
-
this.#displayImageBytes +=
|
|
135
|
+
this.#displayImages.push({ type: "image", mimeType: image.mimeType, data: image.data });
|
|
136
|
+
this.#displayImageBytes += image.data.length;
|
|
110
137
|
return;
|
|
111
138
|
}
|
|
112
139
|
const text = Buffer.from(message.dataBase64, "base64").toString("utf8");
|
package/src/tool/render.ts
CHANGED
|
@@ -73,9 +73,9 @@ const TOOL_CALL_PREVIEW_COUNT = 5;
|
|
|
73
73
|
const TOOL_CALL_COLLAPSED_VISUAL_LINES = 4;
|
|
74
74
|
const TOOL_CALL_COLLAPSED_ERROR_CODE_POINTS = 512;
|
|
75
75
|
const TOOL_ERROR_OMISSION_MARKER = "[tool error omitted]";
|
|
76
|
-
const
|
|
76
|
+
const LIVE_RENDER_TICK_MS = 100;
|
|
77
77
|
// A live row repaints on every tick, so this many ticks without a render means the row is gone.
|
|
78
|
-
const LIVE_TICKER_MAX_IDLE_TICKS =
|
|
78
|
+
const LIVE_TICKER_MAX_IDLE_TICKS = 600;
|
|
79
79
|
|
|
80
80
|
class PlainTextComponent implements EvalRenderComponent {
|
|
81
81
|
#blocks: readonly RenderBlock[] = [];
|
|
@@ -92,7 +92,7 @@ class PlainTextComponent implements EvalRenderComponent {
|
|
|
92
92
|
* The host only animates tool rows for streaming args, `task`, and results carrying
|
|
93
93
|
* `details.progress`; an eval row matches none of them, so nothing repaints it between
|
|
94
94
|
* update events. While a cell is non-terminal this drives the repaint itself so the
|
|
95
|
-
* header's elapsed time
|
|
95
|
+
* header's spinner and elapsed time advance, with no tool updates or RPC traffic. Detached
|
|
96
96
|
* and terminal cards never arm it, and a ticker whose row stopped rendering (transcript
|
|
97
97
|
* rebuild, session switch) stops itself after LIVE_TICKER_MAX_IDLE_TICKS and rearms on
|
|
98
98
|
* the next render, so dropped rows cannot accumulate intervals.
|
|
@@ -115,7 +115,7 @@ class PlainTextComponent implements EvalRenderComponent {
|
|
|
115
115
|
|
|
116
116
|
#armTicker(): void {
|
|
117
117
|
if (this.#ticker !== undefined || this.#invalidate === undefined) return;
|
|
118
|
-
this.#ticker = setInterval(() => this.#tick(),
|
|
118
|
+
this.#ticker = setInterval(() => this.#tick(), LIVE_RENDER_TICK_MS);
|
|
119
119
|
this.#ticker.unref?.();
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -336,7 +336,10 @@ function cellElapsedMs(cell: EvalCellResult, environment: RenderEnvironment): nu
|
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
function cellHeader(cell: EvalCellResult, environment: RenderEnvironment, badges: CellBadges): string {
|
|
339
|
-
const presentation = cellPresentation(
|
|
339
|
+
const presentation = cellPresentation(
|
|
340
|
+
cell.status,
|
|
341
|
+
environment.spinnerFrame ?? Math.floor((cellElapsedMs(cell, environment) ?? 0) / LIVE_RENDER_TICK_MS),
|
|
342
|
+
);
|
|
340
343
|
const runtimeBadge = cell.runtime === undefined ? "" : ` (${formatRuntimeBadge(cell.language, cell.runtime)})`;
|
|
341
344
|
let header = `eval ${cell.language}${runtimeBadge} ${presentation.label} ${presentation.icon}`;
|
|
342
345
|
if (cell.queuedBehind !== undefined && cell.queuedBehind.length > 0)
|
|
@@ -207,8 +207,8 @@ async function executeCell(
|
|
|
207
207
|
};
|
|
208
208
|
try {
|
|
209
209
|
const kernel = await execution.wait(options.kernelManager.getKernel(invocation.input.language, onMessage));
|
|
210
|
-
// Computed before the handler so
|
|
211
|
-
//
|
|
210
|
+
// Computed before the handler so its construction-time snapshot captures this cell's capability.
|
|
211
|
+
// Worker messages later restore that snapshot before calling host tools (#1754, #2512).
|
|
212
212
|
const kernelTools = jsKernelTools(kernel, invocation.input.language);
|
|
213
213
|
const runBound = async (): Promise<AgentToolResult<EvalToolDetails>> => {
|
|
214
214
|
const queue = kernel.queueSnapshot();
|
package/src/tool/types.ts
CHANGED
|
@@ -66,8 +66,7 @@ export type EvalControlInput =
|
|
|
66
66
|
|
|
67
67
|
export type EvalToolRequest = EvalToolInput | EvalControlInput;
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
// actions share it; the description teaches the requirement and parseEvalRequest enforces it.
|
|
69
|
+
// Run fields are optional at the root for control calls, but required in the run branch.
|
|
71
70
|
const LANGUAGE_FIELD_DESCRIPTION =
|
|
72
71
|
"REQUIRED for run. Kernel that runs the cell; each language keeps its own persistent state across eval calls.";
|
|
73
72
|
const CODE_FIELD_DESCRIPTION = "REQUIRED for run. Cell body, verbatim.";
|
|
@@ -124,11 +123,20 @@ export function createEvalInputSchema(
|
|
|
124
123
|
const languages = enabledLanguageList(enabled);
|
|
125
124
|
if (languages.length === 0) throw new Error("eval requires at least one enabled language");
|
|
126
125
|
const languageSchema = evalLanguageUnion(languages);
|
|
126
|
+
const properties = evalInputProperties(languageSchema, deadlines);
|
|
127
127
|
return Type.Unsafe<EvalToolRequest>(
|
|
128
|
-
Type.Object(
|
|
128
|
+
Type.Object(properties, {
|
|
129
|
+
// Keep branches self-contained for Mistral-hosted GLM (#2240).
|
|
129
130
|
anyOf: [
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
Type.Object(
|
|
132
|
+
{ ...properties, action: Type.Optional(Type.Literal("run")) },
|
|
133
|
+
{ required: ["language", "code", "summary"] },
|
|
134
|
+
),
|
|
135
|
+
Type.Object({ action: Type.Literal("list") }),
|
|
136
|
+
Type.Object(
|
|
137
|
+
{ action: Type.Union([Type.Literal("peek"), Type.Literal("stop")]), cell_id: properties.cell_id },
|
|
138
|
+
{ required: ["action", "cell_id"] },
|
|
139
|
+
),
|
|
132
140
|
],
|
|
133
141
|
}),
|
|
134
142
|
) as EvalInputSchema;
|