ITADN

Agent integration via experimental.kernels — MVP shipped, two upstream warts, broader pair-programming design

#568Openhugolytics 创建于 2026-05-18
H
hugolyticscommented
## Use case: external agent driving the editor's kernel via experimental.kernels External agents (Claude Code CLI, Codex, MCP servers, sibling extensions) increasingly want to drive a marimo notebook **in the same kernel the user sees in the editor** — not a sidecar `marimo edit` browser kernel. The `experimental.kernels` API on `marimo-team.vscode-marimo`'s `activate()` return value is exactly the integration point for that (the vscode-jupyter-shaped one mentioned in #545). It works today. I built [`hugolytics/agent-bridge`](https://github.com/hugolytics/agent-bridge) on top of it as a tight existence proof — a ~250 LOC VS Code extension that hosts a localhost HTTP server inside VS Code, maps marimo-pair's SSE protocol onto `experimental.kernels.executeCode`, and exposes notebook cell CRUD via `WorkspaceEdit`. The unmodified [`marimo-pair`](https://github.com/marimo-team/marimo-pair) skill works against it (just `--url http://127.0.0.1:<port>`); 22 of 24 capabilities I drew up pass. Filing this per maintainer guidance on #545: design discussion before public API changes. ## Two upstream warts blocking the MVP These both reproduce against `marimo-team.vscode-marimo@0.13.1` from the marketplace, no fork involved. Each ships with a small targeted PR (RED→GREEN against `upstream/main`). I'm happy to revise the implementation choice per maintainer preference — what matters is grounding the discussion in a verifiable repro + a verifiable fix. ### Wart B — `marimo._code_mode.get_context()` raises from scratchpad **Repro** (anywhere `experimental.kernels.executeCode` is called or `marimo.api` `execute-scratchpad` is invoked): ```python import marimo._code_mode as cm async with cm.get_context() as ctx: pass # → RuntimeError: NotebookDocument not available — code_mode must be invoked # via the /api/execute endpoint which sets the document context variable ``` **Root cause:** `handle_execute_scratchpad` in `marimo._runtime.runtime` populates the `notebook_document_context` ContextVar from `request.notebook_cells`. `execute_scratch` in `marimo_lsp/api.py` currently sends `ExecuteScratchpadCommand(code=args.inner.code)` with no `notebook_cells`, so the ContextVar stays None. **Fix approaches considered:** | | Where | Pros | Cons | |---|---|---|---| | **B1 (proposed)** | `marimo_lsp/api.py` — pass `notebook_cells=tuple(session.app_file_manager.app.cell_manager.document.cells)` on send | 5 LOC. Mirrors `marimo._server/api/endpoints/execution.py:319` literally. Uses the public `cell_manager.document` accessor. | O(n) document snapshot per scratchpad call (negligible for typical notebook size; could be measured if you want). | | B2 | `marimo._runtime.runtime` — have `handle_execute_scratchpad` fall back to a kernel-internal document when `notebook_cells` is None | Callers don't need to remember. | Cross-repo (marimo not marimo-lsp); larger blast radius. | | B3 | `marimo._runtime.commands` — accept a lazy `notebook_cells` callable so snapshot happens only if `code_mode` is actually used | Avoids snapshot cost when unused. | API change in marimo; broader surface. | **Proposed PR:** [`hugolytics:fix/code-mode-scratchpad-context`](https://github.com/hugolytics/marimo-lsp/tree/fix/code-mode-scratchpad-context) (B1). Adds a pytest (`test_scratchpad_code_mode_get_context`) that runs scratchpad code calling `cm.get_context()` and asserts no `marimo-error` arrives and stdout reports the cell count. RED on `main`, GREEN with the patch. ### Wart C — scratchpad exceptions arrive as `application/vnd.code.notebook.stderr`, not `.error` **Repro:** ```ts const ext = vscode.extensions.getExtension("marimo-team.vscode-marimo"); const api = await ext.activate(); const kernel = await api.experimental.kernels.getKernel(notebookUri); for await (const output of kernel.executeCode("1/0")) { for (const item of output.items) { console.log(item.mime, new TextDecoder().decode(item.data)); } } // → application/vnd.code.notebook.stderr ZeroDivisionError: division by zero // → application/vnd.code.notebook.stdout <text/html traceback> // (no application/vnd.code.notebook.error item ever arrives) ``` **Root cause:** `extension/src/kernel/ExecutionRegistry.ts` line 675 (`buildOutputItem`) translates `marimo-error` channel data to `NotebookCellOutputItem.stderr(...)` rather than `.error(...)`. The wrapping `NotebookCellOutput` has `{channel: "marimo-error"}` metadata, but its items don't, so consumers iterating the `AsyncIterable<Output>` from `experimental.kernels.executeCode` can't distinguish a failed run from a successful run that printed to stderr. (vscode-jupyter — the API this is modeled after — uses the `.error` mime for exactly this signal.) **Fix approaches considered:** | | Where | Pros | Cons | |---|---|---|---| | **C1 (proposed)** | `ExecutionRegistry.ts` — `NotebookCellOutputItem.error({name, message})` instead of `.stderr(...)`, where `name = error.exception_type` when `type === "exception"` else `error.type` | Matches vscode-jupyter convention. No API contract change. Python traceback still arrives as a separate console output. | Notebook UI renders `.error()` items as a red error box rather than inline red text — slight UX change. | | C2 | `Api.ts` — add a separate completion-status yield `{success, error?}` after the loop | Explicit success/failure. | Breaks the `AsyncIterable<Output>` contract you intentionally modeled on vscode-jupyter. | | C3 | Consumers regex stderr for tracebacks | No upstream change. | Fragile; conflates warnings with failures; no `{name, message, stack}`. | **Proposed PR:** [`hugolytics:fix/error-mime-type`](https://github.com/hugolytics/marimo-lsp/tree/fix/error-mime-type) (C1). Adds a vitest case asserting `outputs[0].items[0].mime === "application/vnd.code.notebook.error"` for an exception. Updates two existing snapshots that captured the previous mime. 398/398 tests pass; 0 new dependencies. ## Broader pair-programming design — separate threads to follow These don't need decisions in this issue. Surfacing so design conversation can begin in parallel — happy to open dedicated issues if useful. ### D — agentic edit UX inside the editor What should `experimental.kernels` + companion APIs look like for operations beyond "run code": - **Cell mutation** (create / edit / delete) with **permission modes** (`auto` / `always-ask` / `bypass-permissions`) so agents can be given graduated autonomy, mirroring how Claude Code's permission system works. - **Accept/deny diff UX** for cell edits — Copilot-style inline preview, so a user can review what the agent proposes before it lands. - **Undo/redo + edit history** that interleaves agent edits and user edits cleanly. - **Git/worktree integration** — agents working on a feature branch or a separate worktree without colliding with the user's main editing flow. ### E — editor awareness for agents `experimental.kernels.getKernel(uri)` exposes the kernel but not the editor state. Agents would benefit from reading: - **Selected cells** — so they can act on "this cell" without the user repeating context. - **Cell tags** — so a user can mark cells as `do-not-edit`, `agent-scratch`, `expected-output`, etc., and agents can honor those constraints. ### A — static export integration Marimo already exports HTML/PNG/ipynb (via `marimo export`). A small companion API — or a documented pattern around the existing exporter — would let sibling extensions request an export for a given notebook URI without spawning a subprocess. Use case: agents reading rendered plots / table HTML / serialized notebook state for visual / structural inspection, without needing a headless browser in the agent. ### Bridging onboarding — where the HTTP bridge should live long-term The use case that motivates the bridge is narrowly: **a user who has both `marimo-team.vscode-marimo` AND a coding agent (Claude Code, Codex, …) and wants the agent to drive the editor's kernel.** Three other configurations don't need it at all: | Configuration | Needs bridge? | |---|---| | `marimo` standalone + agent (no VS Code) | No — marimo-pair talks to the standalone HTTP server directly | | `marimo` standalone, no agent | No | | VS Code + marimo, no agent | No | | **VS Code + marimo + agent** | **Yes** — otherwise the agent ends up driving a sidecar kernel, not the one the user sees | Today the bridge is a separate sideloaded extension (`hugolytics/agent-bridge`) — that creates onboarding friction and means the user has no signal that they should install it (silently degrades to "agent sees different state than editor"). Proposed long-term home: **bundle the bridge into `marimo-team.vscode-marimo` behind `marimo.agentBridge.enabled` (default off)**, with an onboarding nudge when the marimo extension detects a coding-agent extension installed: > *"Detected Claude Code. Want to enable marimo's agent integration? (You can flip `marimo.agentBridge.enabled` later.)"* Why not bundle in the marimo-pair skill? Because the bridge is editor-side (needs `vscode.commands.executeCommand`, `WorkspaceEdit`, the `experimental.kernels` API), while the skill is agent-side (a shell+protocol contract). Splitting them keeps each side single-responsibility. To make this concrete and friction-free for the maintainer side: [`hugolytics/agent-bridge`](https://github.com/hugolytics/agent-bridge) is ~430 LOC of TypeScript in one file, no runtime deps beyond `vscode`. The repo ships an [`INTEGRATION.md`](https://github.com/hugolytics/agent-bridge/blob/main/INTEGRATION.md) documenting a 4-step vendor: copy `src/extension.ts`, merge ~30 lines of `package.json` contributes, drop the now-redundant `activationEvents`/`extensionDependencies`, add the onboarding nudge. The current standalone extension already implements lazy activation (`onNotebook:marimo-notebook` + `extensionDependencies: ["marimo-team.vscode-marimo"]` + `agentBridge.enabled` setting) so users on the current artifact get the same UX shape as the bundled version would. **Happy to transfer the repo to `marimo-team` if that direction is preferred over a vendor.** ### Transport — HTTP vs MCP Open design question. Today the bridge speaks `marimo-pair`-compatible HTTP/SSE so the existing skill works unmodified. MCP is an alternative worth weighing: | | HTTP/SSE (today) | MCP | |---|---|---| | **Compatibility** | `marimo-pair` skill works unmodified | Requires agent to speak MCP (Claude / Codex do; arbitrary shell agents don't) | | **Streaming** | SSE; established pattern; trivially curl-able | Per-tool streaming via `tool.stream` (newer; less mature tooling) | | **Connection lifecycle** | Stateless HTTP requests; one server, many clients | Stateful connection; awkward to match VS Code extension activation/deactivation | | **State model** | URL-addressable resources (`/notebooks/cells`, `/api/sessions`) | Resource/tool/prompt model; cell-CRUD as 4 tools is fine; SSE-style streaming output of cell-ops less clean | | **Discoverability for agents** | marimo-pair-style registry file | MCP-config in agent's config | | **Maturity** | Plain HTTP, deterministic | Spec moving fast, agents implementing variably | My read: HTTP/SSE remains the right transport for now because (a) the marimo-pair skill is already a working agent-side protocol and we'd lose that compatibility on MCP, and (b) the lifecycle mismatch between MCP connections and VS Code extension activation is real. MCP becomes interesting once the editor-side API surface stabilizes — at which point both transports can coexist (bridge offers both endpoints). Worth a separate design issue if maintainers think MCP-first is the right long-term bet. --- Happy to break any of these into separate threads. --- Tagged: @manzt (from #545).
3 条评论