From 4eb94125b202ecd63fdda3051dd4bafc771ccf39 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 27 Jul 2026 07:28:02 +0200 Subject: [PATCH] fix(coding-agent/eval): filtered internal runner frames from python cell error tracebacks - Filter out runner-internal frames from runtime exception tracebacks to start at user code. - Omit full tracebacks for cell syntax errors to render only the caret display with `` filename. --- packages/coding-agent/CHANGELOG.md | 4 ++++ packages/coding-agent/src/eval/py/runner.py | 18 ++++++++++++++++-- .../core/python-runner.integration.test.ts | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a6343e17e..ce8a4f32d 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Made the task tool's per-spawn `effort` parameter opt-in through `task.enableEffort`, which defaults to false and omits the field from flat and batch schemas and tool guidance until enabled. +### Fixed + +- Fixed Python cell errors (`$` commands and the eval tool) leaking runner-internal traceback frames. Cell syntax errors now render as the bare caret display with a `` filename instead of a `_handle_request_async`/`ast.parse` stack dump, and runtime tracebacks start at user code, matching the Ruby runner's user-frame filtering. + ## [17.1.5] - 2026-07-27 ### Added diff --git a/packages/coding-agent/src/eval/py/runner.py b/packages/coding-agent/src/eval/py/runner.py index e9f116728..7190594c3 100644 --- a/packages/coding-agent/src/eval/py/runner.py +++ b/packages/coding-agent/src/eval/py/runner.py @@ -1057,7 +1057,7 @@ async def _run_compiled_async(code, ns: dict, *, want_value: bool) -> Any: def _compile_source(source: str) -> tuple[Any, Any | None, bool]: - module = ast.parse(source, mode="exec") + module = ast.parse(source, "", "exec") if not module.body: return None, None, False @@ -1279,7 +1279,21 @@ async def _handle_request_async(req: dict) -> None: def _emit_error(rid: str, exc: BaseException) -> None: - tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__) + if isinstance(exc, SyntaxError) and exc.filename == "": + # Syntax error in the cell source itself: every stack frame is runner + # machinery, so emit only the caret display, like a REPL. + tb_lines = traceback.format_exception_only(type(exc), exc) + else: + # Drop the leading runner-internal frames (_handle_request_async -> + # _exec_source_async -> _run_compiled_*) so tracebacks start at user + # code. If the exception never reached user code it is a runner bug; + # keep the full traceback because those frames are the diagnosis. + tb = exc.__traceback__ + while tb is not None and tb.tb_frame.f_code.co_filename == __file__: + tb = tb.tb_next + tb_lines = traceback.format_exception( + type(exc), exc, tb if tb is not None else exc.__traceback__ + ) _emit( { "type": "error", diff --git a/packages/coding-agent/test/core/python-runner.integration.test.ts b/packages/coding-agent/test/core/python-runner.integration.test.ts index a564d8042..6285af66b 100644 --- a/packages/coding-agent/test/core/python-runner.integration.test.ts +++ b/packages/coding-agent/test/core/python-runner.integration.test.ts @@ -123,6 +123,25 @@ describe.skipIf(!SHOULD_RUN)("python runner subprocess", () => { expect(result.exitCode).toBe(1); expect(result.output).toContain("ValueError"); expect(result.output).toContain("boom"); + // Traceback starts at user code; runner-internal frames stay hidden. + expect(result.output).toContain('File ""'); + expect(result.output).not.toContain("_exec_source_async"); + } finally { + await kernel.shutdown(); + } + }); + + it("reports cell syntax errors without runner-internal frames", async () => { + using tempDir = TempDir.createSync("@python-runner-syntax-"); + const kernel = await PythonKernel.start({ cwd: tempDir.path() }); + try { + const result = await executePythonWithKernel(kernel, 'echo "hi"'); + expect(result.exitCode).toBe(1); + expect(result.output).toContain("SyntaxError"); + expect(result.output).toContain('File ""'); + // Caret display only — no stack header, no runner machinery. + expect(result.output).not.toContain("Traceback (most recent call last"); + expect(result.output).not.toContain("_compile_source"); } finally { await kernel.shutdown(); }