From 380046ceb31e9f2df46991e69a0fe9a1d1145ec9 Mon Sep 17 00:00:00 2001 From: MoerAI Date: Sun, 6 Sep 2026 03:33:23 +0900 Subject: [PATCH] fix(coding-agent): explain eval language selection (fixes #1395) Describe the run-only language requirement without choosing a default kernel. Report unsupported values separately from omissions. Verify the regression red-to-green, 35 related tests, full checks and build, and an isolated real CLI eval turn. --- packages/senpi-codemode/changes.md | 9 +++++++ .../senpi-codemode/src/tool/eval-request.ts | 13 +++++++-- packages/senpi-codemode/src/tool/types.ts | 14 +++++++--- .../test/eval-request-language.test.ts | 27 +++++++++++++++++++ 4 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 packages/senpi-codemode/test/eval-request-language.test.ts diff --git a/packages/senpi-codemode/changes.md b/packages/senpi-codemode/changes.md index efe6f368be..ff543687ff 100644 --- a/packages/senpi-codemode/changes.md +++ b/packages/senpi-codemode/changes.md @@ -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 diff --git a/packages/senpi-codemode/src/tool/eval-request.ts b/packages/senpi-codemode/src/tool/eval-request.ts index f47e43329d..c943f594d6 100644 --- a/packages/senpi-codemode/src/tool/eval-request.ts +++ b/packages/senpi-codemode/src/tool/eval-request.ts @@ -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"]); @@ -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) diff --git a/packages/senpi-codemode/src/tool/types.ts b/packages/senpi-codemode/src/tool/types.ts index b5a9c40c48..d9914755c0 100644 --- a/packages/senpi-codemode/src/tool/types.ts +++ b/packages/senpi-codemode/src/tool/types.ts @@ -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."; + 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."; @@ -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( @@ -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( Type.Object({ action: Type.Optional( diff --git a/packages/senpi-codemode/test/eval-request-language.test.ts b/packages/senpi-codemode/test/eval-request-language.test.ts new file mode 100644 index 0000000000..1b4965f851 --- /dev/null +++ b/packages/senpi-codemode/test/eval-request-language.test.ts @@ -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" }); + }); +});