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 `<cell>` filename.
This commit is contained in:
can1357
2026-07-27 07:28:02 +02:00
parent a9b7df8821
commit 4eb94125b2
3 changed files with 39 additions and 2 deletions
+4
View File
@@ -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 `<cell>` 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
+16 -2
View File
@@ -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, "<cell>", "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 == "<cell>":
# 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",
@@ -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 "<cell>"');
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 "<cell>"');
// 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();
}