chore: updated docs
This commit is contained in:
+94
-125
@@ -1,77 +1,79 @@
|
||||
# Notebook tool runtime internals
|
||||
# Notebook file runtime internals
|
||||
|
||||
This document describes the current `notebook` tool implementation and its relationship to the kernel-backed Python runtime.
|
||||
This document describes current `.ipynb` handling in `coding-agent` and its relationship to the kernel-backed Python runtime.
|
||||
|
||||
The critical distinction: **`notebook` is a JSON notebook editor, not a notebook executor**. It edits `.ipynb` cell sources directly; it does not start or talk to a Python kernel.
|
||||
The critical distinction: **notebook support is file conversion/editing, not notebook execution**. `.ipynb` files are exposed as editable cell-marked text through `read` and the edit pipeline; no notebook-specific tool starts or talks to a Python kernel.
|
||||
|
||||
## Implementation files
|
||||
|
||||
- [`src/edit/notebook.ts`](../packages/coding-agent/src/edit/notebook.ts)
|
||||
- [`src/edit/read-file.ts`](../packages/coding-agent/src/edit/read-file.ts)
|
||||
- [`src/tools/read.ts`](../packages/coding-agent/src/tools/read.ts)
|
||||
- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts)
|
||||
- [`src/eval/py/executor.ts`](../packages/coding-agent/src/eval/py/executor.ts)
|
||||
- [`src/eval/py/kernel.ts`](../packages/coding-agent/src/eval/py/kernel.ts)
|
||||
- [`src/session/streaming-output.ts`](../packages/coding-agent/src/session/streaming-output.ts)
|
||||
- [`src/tools/eval.ts`](../packages/coding-agent/src/tools/eval.ts)
|
||||
|
||||
## 1) Runtime boundary: editing vs executing
|
||||
|
||||
## `notebook` tool (`src/edit/notebook.ts`)
|
||||
## `.ipynb` file conversion (`src/edit/notebook.ts`)
|
||||
|
||||
- Supports `action: edit | insert | delete` on a `.ipynb` file.
|
||||
- Resolves path relative to session CWD (`resolveToCwd`).
|
||||
- Loads notebook JSON, validates `cells` array, validates `cell_index` bounds.
|
||||
- Applies source edits in-memory and writes full notebook JSON back with `JSON.stringify(notebook, null, 1)`.
|
||||
- Returns textual summary + structured `details` (`action`, `cellIndex`, `cellType`, `totalCells`, `cellSource`).
|
||||
- `read` treats `.ipynb` files as notebooks unless the selector is `:raw`.
|
||||
- The default notebook view is editable text with markers:
|
||||
- `# %% [code] cell:N`
|
||||
- `# %% [markdown] cell:N`
|
||||
- `# %% [raw] cell:N`
|
||||
- Line selectors and multi-range selectors operate on that virtual text.
|
||||
- Edit/write paths round-trip virtual text back to notebook JSON through `serializeEditedNotebookText(...)`.
|
||||
- Existing notebook metadata is preserved when a marker references an existing `cell:N`; new cells get fresh empty metadata.
|
||||
- Missing notebooks edited through this path start from an empty nbformat 4.5 notebook.
|
||||
|
||||
No kernel lifecycle exists in this tool:
|
||||
No kernel lifecycle exists in this path:
|
||||
|
||||
- no gateway acquisition
|
||||
- no kernel session ID
|
||||
- no `execute_request`
|
||||
- no stream chunks from kernel channels
|
||||
- no rich display capture (`image/png`, JSON display, status MIME)
|
||||
- no code execution
|
||||
- no stream chunks from Python
|
||||
- no rich display capture
|
||||
- no output artifact pipeline from execution
|
||||
|
||||
## Notebook-like execution path (`src/tools/eval.ts` + `src/eval/py/*`)
|
||||
## Kernel-backed execution path (`src/tools/eval.ts` + `src/eval/py/*`)
|
||||
|
||||
When the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with `language: "python"`, not `notebook`.
|
||||
When the agent needs to run cell-style Python code (sequential cells, persistent state, rich displays), that goes through the **`eval` tool** with per-cell `language: "py"`, not through notebook file handling.
|
||||
|
||||
That path is where kernel modes, restart/cancel behavior, chunk streaming, and output artifact truncation live.
|
||||
That path is where Python subprocess lifecycle, reset/cancel behavior, chunk streaming, rich displays, and output artifact truncation live.
|
||||
|
||||
## 2) Notebook cell handling semantics (`notebook` tool)
|
||||
## 2) Notebook cell handling semantics
|
||||
|
||||
## Source normalization
|
||||
|
||||
`content` is split into `source: string[]` with newline preservation:
|
||||
Notebook JSON `source` is converted to virtual text by joining source arrays. When virtual text is serialized back, cell source is split with newline preservation:
|
||||
|
||||
- each non-final line keeps trailing `\n`
|
||||
- final line has no forced trailing newline
|
||||
- each line ending in `\n` stays as a separate source entry with the newline
|
||||
- a final non-newline-terminated line is stored without forcing a trailing newline
|
||||
- empty content becomes an empty `source` array
|
||||
|
||||
This mirrors notebook JSON conventions and avoids accidental line concatenation on later edits.
|
||||
|
||||
## Action behavior
|
||||
## Marker parsing and cell preservation
|
||||
|
||||
- `edit`
|
||||
- replaces `cells[cell_index].source`
|
||||
- preserves existing `cell_type`
|
||||
- `insert`
|
||||
- inserts at `[0..cellCount]`
|
||||
- `cell_type` defaults to `code`
|
||||
- code cells initialize `execution_count: null` and `outputs: []`
|
||||
- markdown cells initialize only `metadata` + `source`
|
||||
- `delete`
|
||||
- removes `cells[cell_index]`
|
||||
- returns removed `source` in details for renderer preview
|
||||
- The first representation line must be a marker; text before the first marker, including a blank line, is rejected.
|
||||
- Markers must match `# %% [code|markdown|raw]` with optional `cell:N`.
|
||||
- If `cell:N` points at an unused existing cell, that cell is cloned, its `cell_type` and `source` are updated, and unrelated metadata is preserved.
|
||||
- If no valid unused original index is present, a new cell is created.
|
||||
- Code cells ensure `execution_count` exists and `outputs` exists.
|
||||
- Markdown/raw cells remove `execution_count` and `outputs`.
|
||||
|
||||
## Error surfaces
|
||||
|
||||
Hard failures are thrown for:
|
||||
|
||||
- missing notebook file
|
||||
- missing notebook on read
|
||||
- invalid JSON
|
||||
- missing/non-array `cells`
|
||||
- out-of-range index (insert and non-insert have different valid ranges)
|
||||
- missing `content` for `edit`/`insert`
|
||||
- invalid cell objects or cell types
|
||||
- invalid editable representation (for example, text before the first cell marker)
|
||||
|
||||
These become `Error:` tool responses upstream; renderer uses notebook path + formatted error text.
|
||||
These surface through the caller (`read`, edit, or `write`) as normal tool errors.
|
||||
|
||||
## 3) Kernel session semantics (where they actually exist)
|
||||
|
||||
@@ -82,136 +84,103 @@ Kernel semantics are implemented in `executePython` / `PythonKernel` and apply t
|
||||
`PythonKernelMode`:
|
||||
|
||||
- `session` (default)
|
||||
- kernels cached in `kernelSessions` map
|
||||
- max 4 sessions; oldest evicted on overflow
|
||||
- idle/dead cleanup every 30s, timeout after 5 minutes
|
||||
- per-session queue serializes execution (`session.queue`)
|
||||
- kernels are cached by `(session id, cwd)`
|
||||
- multiple owners can share a retained kernel for the same key
|
||||
- execution is serialized by the tool's exclusive concurrency and backend execution path
|
||||
- dead kernels are replaced before execution
|
||||
- `per-call`
|
||||
- creates kernel for request
|
||||
- creates a subprocess for the request
|
||||
- executes
|
||||
- always shuts down kernel in `finally`
|
||||
- always shuts down the subprocess in `finally`
|
||||
|
||||
## Reset behavior
|
||||
|
||||
`eval` passes `reset` only for the first cell in a multi-cell Python call; later cells always run with `reset: false`.
|
||||
Each eval cell has its own optional `reset` flag. `reset: true` resets the selected Python session before that cell executes; it is not a top-level tool parameter.
|
||||
|
||||
## Kernel death / restart / retry
|
||||
|
||||
In session mode (`withKernelSession`):
|
||||
In session mode:
|
||||
|
||||
- dead kernel detected by heartbeat (`kernel.isAlive()` check every 5s) or execute failure.
|
||||
- pre-run dead state triggers `restartKernelSession`.
|
||||
- execute-time crash path retries once: restart kernel, rerun handler.
|
||||
- `restartCount > 1` in same session throws `Python kernel restarted too many times in this session`.
|
||||
|
||||
Startup retry behavior:
|
||||
|
||||
- shared gateway kernel creation retries once on `SharedGatewayCreateError` with HTTP 5xx.
|
||||
|
||||
Resource exhaustion recovery:
|
||||
|
||||
- detects `EMFILE`/`ENFILE`/"Too many open files" style failures
|
||||
- clears tracked sessions
|
||||
- calls `shutdownSharedGateway()`
|
||||
- retries kernel session creation once
|
||||
- if the retained subprocess is not alive before execution, it is replaced
|
||||
- if execution fails because the subprocess died, the kernel is replaced and the code is retried once
|
||||
- explicit `reset` is rejected while another reset for the same session key is already in progress
|
||||
|
||||
## 4) Environment/session variable injection
|
||||
|
||||
Kernel startup receives the optional session file path from executor:
|
||||
Kernel startup and per-execution environment patching can receive:
|
||||
|
||||
- `PI_SESSION_FILE` (session state file path)
|
||||
- `PI_SESSION_FILE`
|
||||
- `PI_ARTIFACTS_DIR`
|
||||
- `PI_TOOL_BRIDGE_URL`
|
||||
- `PI_TOOL_BRIDGE_TOKEN`
|
||||
- `PI_TOOL_BRIDGE_SESSION`
|
||||
|
||||
`PythonKernel.#initializeKernelEnvironment(...)` then runs init script inside kernel to:
|
||||
|
||||
- `os.chdir(cwd)`
|
||||
- inject env entries into `os.environ`
|
||||
- prepend cwd to `sys.path` if missing
|
||||
|
||||
Implication:
|
||||
|
||||
- prelude helpers that read session context rely on this env var in Python process state.
|
||||
The runner initializes process state so code executes in the requested cwd, managed env entries are reflected in `os.environ`, and cwd is available on `sys.path`.
|
||||
|
||||
## 5) Streaming/chunk and display handling (kernel-backed path)
|
||||
|
||||
The kernel client processes Jupyter protocol messages per execution:
|
||||
The Python backend uses an NDJSON subprocess runner. The host processes frames per execution:
|
||||
|
||||
- `stream` -> text chunk to `onChunk`
|
||||
- `execute_result` / `display_data` ->
|
||||
- display text chosen by MIME precedence: `text/markdown` > `text/plain` > converted `text/html`
|
||||
- structured outputs captured separately:
|
||||
- `application/json` -> `{ type: "json" }`
|
||||
- `image/png` -> `{ type: "image" }`
|
||||
- `application/x-omp-status` -> `{ type: "status" }` (no text emission)
|
||||
- `error` -> traceback text pushed to chunk stream + structured error metadata
|
||||
- `input_request` -> emits stdin warning text, sends empty `input_reply`, marks stdin requested
|
||||
- completion waits for both `execute_reply` and kernel `status=idle`
|
||||
- `stdout` / `stderr` -> text chunks to `onChunk`
|
||||
- `display` / `result` -> MIME bundle rendering
|
||||
- `error` -> traceback text and structured error metadata
|
||||
- `done` -> final status, execution count, cancellation state
|
||||
|
||||
Display text MIME precedence:
|
||||
|
||||
1. `text/markdown`
|
||||
2. `text/plain`
|
||||
3. converted `text/html`
|
||||
|
||||
Structured outputs captured separately include:
|
||||
|
||||
- `application/json` -> JSON display output
|
||||
- `image/png` / `image/jpeg` -> image output
|
||||
- `application/x-omp-status` -> status event
|
||||
|
||||
Cancellation/timeout:
|
||||
|
||||
- abort signal triggers `interrupt()` (REST `/interrupt` + control-channel `interrupt_request`)
|
||||
- result marks `cancelled=true`
|
||||
- timeout path annotates output with `Command timed out after <n> seconds`
|
||||
- abort/timeout sends `SIGINT` to the runner
|
||||
- if the runner does not settle after the interrupt grace window, shutdown escalates and the kernel is recreated on the next call
|
||||
- timeout output is annotated with a timeout message
|
||||
|
||||
## 6) Truncation and artifact behavior
|
||||
|
||||
`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths (`executeWithKernel`):
|
||||
`OutputSink` in `src/session/streaming-output.ts` is used by kernel execution paths:
|
||||
|
||||
- sanitizes every chunk (`sanitizeText`)
|
||||
- sanitizes every chunk
|
||||
- tracks total/output lines and bytes
|
||||
- optional artifact spill file (`artifactPath`, `artifactId`)
|
||||
- when in-memory buffer exceeds threshold (`DEFAULT_MAX_BYTES` unless overridden):
|
||||
- marks truncated
|
||||
- keeps tail bytes in memory (UTF-8 safe boundary)
|
||||
- can spill full stream to artifact sink
|
||||
|
||||
`dump()` returns:
|
||||
|
||||
- visible output text (possibly tail-truncated)
|
||||
- truncation flag + counts
|
||||
- artifact ID (for `artifact://<id>` references)
|
||||
- optionally spills full output to an artifact file
|
||||
- keeps a UTF-8-safe in-memory tail buffer when output exceeds the configured threshold
|
||||
|
||||
`eval` converts this metadata into result truncation notices and TUI warnings.
|
||||
|
||||
`notebook` tool does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code.
|
||||
Notebook file conversion does **not** use `OutputSink`; it has no stream/artifact truncation pipeline because it does not execute code.
|
||||
|
||||
## 7) Renderer assumptions and formatting
|
||||
|
||||
## Notebook renderer (`notebookToolRenderer`)
|
||||
## Read/edit notebook representation
|
||||
|
||||
- call view: status line with action + notebook path + cell/type metadata
|
||||
- result view:
|
||||
- success summary derived from `details`
|
||||
- `cellSource` rendered via `renderCodeCell`
|
||||
- markdown cells set language hint `markdown`; other cells have no explicit language override
|
||||
- collapsed code preview limit is `PREVIEW_LIMITS.COLLAPSED_LINES * 2`
|
||||
- supports expanded mode via shared render options
|
||||
- uses render cache keyed by width + expanded state
|
||||
|
||||
Error rendering assumption:
|
||||
|
||||
- if first text content starts with `Error:`, renderer formats as notebook error block.
|
||||
Notebook files are rendered to the model as text. The visible cell markers are part of the editable representation, not comments that are ignored during serialization.
|
||||
|
||||
## Python renderer (for actual execution output)
|
||||
|
||||
Kernel-backed execution rendering expects:
|
||||
|
||||
- per-cell status transitions (`pending/running/complete/error`)
|
||||
- optional structured status event section
|
||||
- per-cell status transitions (`pending` / `running` / `complete` / `error`)
|
||||
- optional structured status events
|
||||
- optional JSON output trees
|
||||
- image outputs
|
||||
- truncation warnings + optional `artifact://<id>` pointer
|
||||
|
||||
This renderer behavior is unrelated to `notebook` JSON editing results except that both reuse shared TUI primitives.
|
||||
This renderer behavior is unrelated to notebook JSON editing except that both reuse shared TUI primitives.
|
||||
|
||||
## 8) Divergence from eval Python backend behavior
|
||||
## 8) Practical workflow
|
||||
|
||||
If "plain Python execution" means the `eval` tool with `language: "python"`:
|
||||
If a workflow needs both notebook mutation and execution:
|
||||
|
||||
- `eval` executes code in a kernel, persists state by mode, streams chunks, captures rich displays, handles interrupts/timeouts, and supports output truncation/artifacts.
|
||||
- `notebook` performs deterministic notebook JSON mutations only; no execution, no kernel state, no chunk stream, no display outputs, no artifact pipeline.
|
||||
|
||||
If a workflow needs both:
|
||||
|
||||
1. edit notebook source with `notebook`
|
||||
2. execute code cells via `eval` with `language: "python"` (manually passing code), not through `notebook`
|
||||
1. read or edit the `.ipynb` file through the normal file tools
|
||||
2. copy the desired cell source into `eval` cells with `language: "py"` to execute it
|
||||
3. write resulting source changes back to the notebook if needed
|
||||
|
||||
Current implementation does not provide a single tool that both mutates `.ipynb` and executes notebook cells through kernel context.
|
||||
|
||||
Reference in New Issue
Block a user