Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/senpi-codemode/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# senpi-codemode fork changes

## 2026-09-05 - Explain eval's run-only language requirement

- The live and exported eval schemas describe `language` as required for runs,
with no default kernel, while keeping it optional for `peek` and `stop`.
- Request parsing distinguishes an omitted language from an unsupported value
and lists the supported language identifiers for invalid values.
- Regression: `test/eval-request-language.test.ts` covers distinct diagnostics,
omitted run languages, and language-free control requests. Fixes #1395.

## 2026-09-05 - GPT eval dialect routes waits through tool.monitor

### What changed
Expand Down
13 changes: 11 additions & 2 deletions packages/senpi-codemode/src/tool/eval-request.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { ExtensionContext } from "@code-yeongyu/senpi";
import { EVAL_SUMMARY_MAX_LENGTH, type EvalControlInput, type EvalToolInput, type EvalToolRequest } from "./types.ts";
import {
EVAL_SUMMARY_MAX_LENGTH,
type EvalControlInput,
type EvalToolInput,
type EvalToolRequest,
evalLanguageOrder,
} from "./types.ts";

const NON_INTERACTIVE_MODES = new Set(["print", "json"]);

Expand All @@ -25,7 +31,10 @@ export function parseEvalRequest(params: unknown): EvalToolRequest {
}
if (params.action !== undefined && params.action !== "run")
throw new TypeError(`Unknown eval action "${String(params.action)}"`);
if (!isEvalLanguage(params.language)) throw new TypeError("eval run requires language");
if (params.language === undefined) throw new TypeError("eval run requires language");
if (!isEvalLanguage(params.language)) {
throw new TypeError(`eval run language must be one of: ${evalLanguageOrder.join(", ")}`);
}
if (typeof params.code !== "string") throw new TypeError("eval run requires code");
const summary = clampEvalSummary(params.summary);
if (summary === undefined)
Expand Down
14 changes: 11 additions & 3 deletions packages/senpi-codemode/src/tool/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export function enabledLanguageList(enabled: EnabledEvalLanguages): EvalLanguage

export const EVAL_SUMMARY_MAX_LENGTH = 80;

const LANGUAGE_FIELD_DESCRIPTION =
"REQUIRED for run. Choose a kernel explicitly; there is no default. Omit for peek/stop.";
Comment on lines +16 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the language requirement in the README

This introduces a user-visible contract stating that every run must select a language and that no default kernel exists, but packages/senpi-codemode/README.md still only lists available kernels and never documents that requirement. Users and extension integrators relying on the shipped README therefore remain exposed to the ambiguity this change is intended to resolve; update the README alongside the schema.

AGENTS.md reference: packages/senpi-codemode/AGENTS.md:L76-L77

Useful? React with 👍 / 👎.


const TIMEOUT_FIELD_DESCRIPTION =
"Seconds the cell may block the turn before it detaches, and the amount by which it raises the wall-clock hard limit. In interactive sessions the detach point is capped at the foreground window (default 60s), so a large value frees the turn at the window while the cell keeps running; on_timeout:'error' (and print/json) keep the full value as the uncapped deadline.";

Expand Down Expand Up @@ -43,7 +46,9 @@ const fullEvalInputSchema = Type.Object({
}),
),
language: Type.Optional(
Type.Union([Type.Literal("js"), Type.Literal("py"), Type.Literal("rb"), Type.Literal("jl")]),
Type.Union([Type.Literal("js"), Type.Literal("py"), Type.Literal("rb"), Type.Literal("jl")], {
description: LANGUAGE_FIELD_DESCRIPTION,
}),
),
code: Type.Optional(Type.String({ description: "Cell body, verbatim." })),
summary: Type.Optional(
Expand Down Expand Up @@ -71,8 +76,11 @@ export function createEvalInputSchema(enabled: EnabledEvalLanguages): EvalInputS
if (languages.length === 0) throw new Error("eval requires at least one enabled language");
const languageSchema =
languages.length === 1
? Type.Union([Type.Literal(languages[0])])
: Type.Union(languages.map((item) => Type.Literal(item)));
? Type.Union([Type.Literal(languages[0])], { description: LANGUAGE_FIELD_DESCRIPTION })
: Type.Union(
languages.map((item) => Type.Literal(item)),
{ description: LANGUAGE_FIELD_DESCRIPTION },
);
return Type.Unsafe<EvalToolRequest>(
Type.Object({
action: Type.Optional(
Expand Down
27 changes: 27 additions & 0 deletions packages/senpi-codemode/test/eval-request-language.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest";
import { parseEvalRequest } from "../src/tool/eval-request.ts";

function languageError(language: unknown): TypeError {
try {
parseEvalRequest({ language, code: "return 1", summary: "Evaluate a number" });
} catch (error) {
if (error instanceof TypeError) return error;
throw error;
}
throw new Error("Expected an invalid run request to be rejected");
}

// Regression for #1395: callers must distinguish an omitted language from an invalid value.
describe("eval request language validation", () => {
it.each([null, "", "python", 42])("distinguishes invalid language %j from an omission", (language) => {
expect(languageError(language).message).not.toBe(languageError(undefined).message);
});

it("rejects an omitted language instead of selecting a default kernel", () => {
expect(() => parseEvalRequest({ code: "return 1", summary: "Evaluate a number" })).toThrow(TypeError);
});

it.each(["peek", "stop"])("accepts %s without a language", (action) => {
expect(parseEvalRequest({ action, cell_id: "cell-1395" })).toEqual({ action, cell_id: "cell-1395" });
});
});