diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e03f1f3..1a4840b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 - `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`) - Fix: Duplicate comments in re-saved YAML configuration files #1285 + - Prompt provision improvements (project activation, initial instructions): + - Prompt provision is now session-aware, i.e. when using the MCP server in HTTP mode, prompts are provided for each session separately, + ensuring that the necessary information is always provided to the LLM + - Fix: Prompts of dynamically activated modes (upon project activation) were not necessarily passed to the LLM (only in the system prompt via + `initial_instructions`). Now they are passed directly in the activation message (and excluded from a subsequent `initial_instructions` call). + - Fix: Project activation message was provided more than once for case of dynamic project activation followed + by `initial_instructions` #1372 + - Always provide full activation message upon calling `activate_project` (even if project was already active in the same session) #1384 + This is necessary, because some clients (e.g. Claude Desktop) will reuse a single session across chats. + - Security: Forbid `".."` in memory names to disallow accessing files outside dedicated memory directories + - Security: Add check for tool being read-only in the project server (previously only checked in `query_project` tool, i.e. client side) + - Usage reporting now also includes the name of the Serena context that is used + - Fix: restricted `insert_after_symbol` to raise if used on an assignment or similar (can't reliably determine the symbol range) + - Fix: Failure to collect project ignore spec now logs the error and downstream tasks fail fast, fixing hanging LS initialisation + - Improve loading of `project.yml` files: Gracefully handle user errors involving incorrect use of None/empty instead of list + - Project server: + - `query_project`: Support use of project root instead of project name #1388 + - `list_queryable_projects`: Return both project names and project roots * JetBrains: - `Move` and `SafeDelete` tools: transform empty string to None (counteracts client errors) @@ -14,12 +32,16 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 +* Evaluations: + - Added new evaluations for Junie Plugin with Opus 4.6 and GLM 5.1 in Claude Code. * Language Servers: - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files - Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356 +Dashboard: + - Fix: Memory leaks in frontend when using Chromium-based browsers/Windows webview #1389 # v1.1.2 (2026-04-14) diff --git a/README.md b/README.md index 84cb122f..d26bdad8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ more complex codebases. > Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. > Instead, follow our [Quick Start](#quick-start) instructions. +## Quick Demo + +https://github.com/user-attachments/assets/8d11646e-b80e-4723-b9d7-32d6101b5f58 + +:tv: Longer video: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA) + ## What Our "End Users" Say While it is humans who download and set up Serena, our end users are essentially AI agents. @@ -72,16 +78,12 @@ Most modern AI chat clients directly support MCP, including +:tv: See also: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA) + To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -Serena's tools are powered by two alternative language intelligence backends: - * With the SolidLSP backend (default), language servers for the selected programming languages will be started -automatically for your project. - * With the JetBrains backend, the Serena JetBrains plugin must be installed in your IDE and -the project you want to work on must open and set up. - See the [Quick Start](#quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities @@ -97,7 +99,7 @@ There are two alternative technologies powering these capabilities: * **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative which is used by default. -* The **Serena JetBrains Plugin**, which leverages the powerful code analysis and editing +* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE (paid plugin; free trial available). You can choose either of these backends depending on your preferences and requirements. @@ -203,25 +205,6 @@ To achieve this, Serena offers multiple levels of (composable) configuration: -## Serena in Action - -
-Demonstrations - -#### Demonstration 1: Efficient Operation in Claude Code - -A demonstration of Serena efficiently retrieving and editing code within Claude Code, thereby saving tokens and time. Efficient operations are not only useful for saving costs, but also for generally improving the generated code's quality. This effect may be less pronounced in very small projects, but often becomes of crucial importance in larger ones. - -https://github.com/user-attachments/assets/ab78ebe0-f77d-43cc-879a-cc399efefd87 - -#### Demonstration 2: Serena in Claude Desktop - -A demonstration of Serena implementing a small feature for itself (a better log GUI) with Claude Desktop. -Note how Serena's tools enable Claude to find and edit the right symbols. - -https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 -
- ## Quick Start **Prerequisites**. Serena is managed by *uv*, and [installing uv](https://docs.astral.sh/uv/getting-started/installation/) is the only required prerequisite. diff --git a/docs/01-about/040_comparison-to-other-agents.md b/docs/01-about/040_comparison-to-other-agents.md deleted file mode 100644 index df89149e..00000000 --- a/docs/01-about/040_comparison-to-other-agents.md +++ /dev/null @@ -1,51 +0,0 @@ -# Comparison with Other Coding Agents - -To our knowledge, Serena is the first fully-featured coding agent where the -entire functionality is made available through an MCP server, -thus not requiring additional API keys or subscriptions if access to an LLM -is already available through an MCP-compatible client. - -## Subscription-Based Coding Agents - -Many prominent subscription-based coding agents are parts of IDEs like -Windsurf, Cursor and VSCode. -Serena's functionality is similar to Cursor's Agent, Windsurf's Cascade or -VSCode's agent mode. - -Serena has the advantage of not requiring a subscription. - -More technical differences are: - -* Serena navigates and edits code using a language server, so it has a symbolic - understanding of the code. - IDE-based tools often use a text search-based or purely text file-based approach, which is often - less powerful, especially for large codebases. -* Serena is not bound to a specific interface (IDE or CLI). - Serena's MCP server can be used with any MCP client (including some IDEs). -* Serena is not bound to a specific large language model or API. -* Serena is open-source and has a small codebase, so it can be easily extended - and modified. - -## API-Based Coding Agents - -An alternative to subscription-based agents are API-based agents like Claude -Code, Cline, Aider, Roo Code and others, where the usage costs map directly -to the API costs of the underlying LLM. -Some of them (like Cline) can even be included in IDEs as an extension. -They are often very powerful and their main downside are the (potentially very -high) API costs. -Serena itself can be used as an API-based agent (see the [section on Agno](../03-special-guides/custom_agent.md)). - -The main difference between Serena and other API-based agents is that Serena can -also be used as an MCP server, thus not requiring -an API key and bypassing the API costs. - -## Other MCP-Based Coding Agents - -There are other MCP servers designed for coding, like [DesktopCommander](https://github.com/wonderwhy-er/DesktopCommanderMCP) and -[codemcp](https://github.com/ezyang/codemcp). -However, to the best of our knowledge, none of them provide semantic code -retrieval and editing tools; they rely purely on text-based analysis. -It is the integration of language servers and the MCP that makes Serena unique -and so powerful for challenging coding tasks, especially in the context of -larger codebases. \ No newline at end of file diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 5a0a16f2..1995ca34 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -2,7 +2,7 @@ The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows the Serena MCP server to leverage the powerful code analysis and editing capabilities of your JetBrains IDE. -This page explains how to install the plugin and how to configure Serena for using it. +This page explains how to install the plugin and how to configure Serena appropriately. You will still need to set up the Serena MCP server itself, so make sure to follow the [installation instructions](020_running.md) and connect the MCP server to your LLM-based client as described in [client setup](030_clients.md) in addition to following the instructions below. @@ -18,20 +18,20 @@ LLM-based client as described in [client setup](030_clients.md) in addition to f We recommend the JetBrains plugin as the preferred way of using Serena, especially for users of JetBrains IDEs. -```{admonition} *Note:* The plugin is a language intelligence backend for the Serena MCP server. -:class: note -It is *not* a UI extension for direct agent interaction (like Copilot) or anything of the sort. -You still interact with your regular client – be it external to your IDE (like Claude Code) or internal (like JetBrains AI Assistant) – -and connect it to the Serena MCP server. -The plugin simply enables the Serena MCP server to directly leverage capabilities of your JetBrains IDE! -``` - **How it works:** 1. Install the plugin in your JetBrains IDE 2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) 3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) 4. Start coding via your MCP client as usual +```{admonition} *Note:* The plugin is a language intelligence backend for the Serena MCP server. +:class: note +It is *not* a UI extension for direct agent interaction (like Copilot) or anything of the sort. +You still interact with your regular client – be it external to your IDE (like Claude Code CLI) or internal (like Copilot or JetBrains AI Assistant) – +and connect it to the Serena MCP server. +The plugin simply enables the Serena MCP server to directly leverage capabilities of your JetBrains IDE! +``` + **Purchasing the JetBrains Plugin supports the Serena project.** The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena. @@ -82,7 +82,7 @@ language_backend: JetBrains Note that the file might not exist yet if you never executed Serena before. -**Per-Instance Configuration**. +**Per-Server Instance Configuration**. The configuration setting in the global config file can be overridden on a per-instance basis by providing the arguments `--language-backend JetBrains` when launching the Serena MCP server. @@ -119,9 +119,10 @@ You will also notice that your client will use the JetBrains-specific tools like Having installed the plugin in your IDE and having configured Serena to use the JetBrains backend, the general workflow is simple: -1. Open the project you want to work on in your JetBrains IDE -2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)) -3. Start using Serena tools as usual +1. Open the project you want to work on in your JetBrains IDE. + Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. +2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)). +3. Start using Serena's tools as usual. Note that the project folder that is open in your IDE and the Serena project root folder must match. diff --git a/docs/04-evaluation/000_evaluation-intro.md b/docs/04-evaluation/000_evaluation-intro.md index 64efedee..596685a5 100644 --- a/docs/04-evaluation/000_evaluation-intro.md +++ b/docs/04-evaluation/000_evaluation-intro.md @@ -16,7 +16,7 @@ medium-sized Java project in the case of Codex). Different agents in different settings independently converge on the same core finding: Serena's strongest contribution is collapsing multi-file, semantically-aware operations into single atomic calls, while built-in tools remain the better choice for small local edits, text search, -config files, and shell work. +config files, and shell work. See the full [results](030_results/000_evaluation-results.md) for the scenarios mentioned above and several other scenarios. ## Approach @@ -32,3 +32,16 @@ The agent evaluates itself. This is deliberate: the agent is the actual user of so it can judge workflow improvements from direct experience rather than through proxy metrics. And because the prompt defines task *categories* rather than fixed tasks, anyone can rerun the evaluation on their own project with their own agent. + + +## Serena in JetBrains Junie + +A particularly notable evaluation scenario is Serena in JetBrains' Junie plugin. The latter also has access +to some of JetBrains' refactoring tools. At the time of writing, the only overlapping capability between Serena's tool +and Junie's native tools is the renaming feature. Opus correctly noticed this during the evaluation and marked the +renaming capability as equivalent. However, many symbolic and refactoring tools offered by Serena have no inbuilt equivalents, +leading to the following summary result for Junie: + +> **Junie Plugin (Opus 4.6)**: Serena gives me what my built-in tools can't — the ability to move a function between modules with all imports updated atomically, trace a class hierarchy into dependencies, and safely delete symbols with usage guards — and I'd ask my owner to add it for the move-refactoring and semantic navigation capabilities alone. + +See the full evaluation [here](030_results/050_junie_plugin_on_tianshou.md). diff --git a/docs/04-evaluation/030_results/000_evaluation-results.md b/docs/04-evaluation/030_results/000_evaluation-results.md index eecd29e8..20fecab7 100644 --- a/docs/04-evaluation/030_results/000_evaluation-results.md +++ b/docs/04-evaluation/030_results/000_evaluation-results.md @@ -13,6 +13,8 @@ evaluation can easily be repeated with the LSP-based backend to assess its subse - [Claude Code (Opus 4.6) on a large Python codebase](010_cc_on_tianshou) - [Codex (GPT 5.4) on a Java codebase](020_codex_on_jbplugin) - [Copilot CLI (GPT 5.4) on a large, multi-language monorepo](030_copilot_cli_on_ente.md) +- [GLM 5.1 in Claude Code](040_glm_on_tianshou) +- [JetBrains Junie with Opus 4.6](050_junie_plugin_on_tianshou.md) You can run your own evaluation on a project of your choice by reusing our [evaluation prompt](../020_prompts/010_evaluation-prompt.md). diff --git a/docs/04-evaluation/030_results/010_cc_on_tianshou.md b/docs/04-evaluation/030_results/010_cc_on_tianshou.md index 763f5f6f..f18a9d7a 100644 --- a/docs/04-evaluation/030_results/010_cc_on_tianshou.md +++ b/docs/04-evaluation/030_results/010_cc_on_tianshou.md @@ -2,6 +2,7 @@ :class: note **Generated by**: Claude Opus 4.6 (coding AI agent in Claude Code CLI) **Codebase:** [Tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) +**Date**: 2026-04-13 ::: # Claude Code (Opus 4.6, medium) diff --git a/docs/04-evaluation/030_results/020_codex_on_jbplugin.md b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md index ee0f78d8..d9cb58e1 100644 --- a/docs/04-evaluation/030_results/020_codex_on_jbplugin.md +++ b/docs/04-evaluation/030_results/020_codex_on_jbplugin.md @@ -2,6 +2,7 @@ :class: note **Generated by:** GPT-5.4 (high) in Codex **Codebase:** Serena JetBrains Plugin (Java) +**Date**: 2026-04-13 ::: # Codex (GPT-5.4, high) diff --git a/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md b/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md index bef518e2..df51b69e 100644 --- a/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md +++ b/docs/04-evaluation/030_results/030_copilot_cli_on_ente.md @@ -1,7 +1,8 @@ :::{admonition} Evaluation Result :class: note **Generated by:** GPT-5.4 (medium) in Copilot Cli -**Codebase:** [ente](https://github.com/ente-io/ente) - a large monorepo in Dart, TypeScript, Go, Rust, and other languages. +**Codebase:** [ente](https://github.com/ente-io/ente) - a large monorepo in Dart, TypeScript, Go, Rust, and other languages. +**Date**: 2026-04-14 ::: # Copilot CLI (GPT-5.4, medium) diff --git a/docs/04-evaluation/030_results/040_glm_on_tianshou.md b/docs/04-evaluation/030_results/040_glm_on_tianshou.md new file mode 100644 index 00000000..437ce905 --- /dev/null +++ b/docs/04-evaluation/030_results/040_glm_on_tianshou.md @@ -0,0 +1,446 @@ +:::{admonition} Evaluation Result +:class: note +**Generated by**: GLM 5.1 (coding AI agent in Claude Code CLI) +**Codebase:** [Tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) +**Date**: 2026-04-14 +::: + +# Claude Code (GLM 5.1) + +>Serena's cross-file refactoring turned an 8-step, error-prone rename chain into a single atomic call — and after +measuring the delta across 20 tasks, I would ask any developer I work with to install it, not because it replaces my +built-in tools, but because it makes the hardest part of my job (changing code safely across files) suddenly feel +reliable. + +--- + +## 1. Headline: What Serena Changes + +**(a) Tasks where Serena adds capability.** Cross-file refactorings (rename, move) collapse from N separate read-find-edit-verify cycles into a single atomic call. Semantic queries (type hierarchy, find implementations, find references) return structured, symbol-accurate results that text search cannot produce without manual filtering. Symbol-level addressing (name paths) is stable across edits, eliminating stale-line-number rework in multi-step sessions. + +**(b) Tasks where Serena applies but offers no improvement.** Single-file, small-bore edits (changing an error message, renaming a local variable with `replace_all`, inserting a function when you already know the insertion context) are equally fast with both toolsets. For a 1-line tweak in a 22-line method, Edit sends ~200 chars; Serena's `replace_symbol_body` sends ~800 chars (the entire method body). The overhead reverses for full-body rewrites of large methods, where Serena sends only the new body while Edit sends old+new. + +**(c) Tasks outside Serena's scope.** Reading non-code files (config, TOML, docs), free-text pattern search (`FIXME`, magic constants, log strings), shell operations, and git workflows are all built-in territory. These are not Serena shortcomings — they are outside its design scope. + +**Verdict:** Serena adds substantial, measurable capability in two areas — cross-file refactoring (1 call vs 5–8) and semantic code queries (structured, symbol-accurate results vs flat text matches) — while providing no meaningful delta for small single-file edits or non-code tasks. + +--- + +## 2. Added Value and Differences by Area + +### 2.1 Cross-file renaming: 1 call replaces 5–8 + +- **What changes:** Renaming `CollectStatsBase` to `BaseCollectStats` across 4 files (10 occurrences) required 1 Serena call vs 1 Grep + 3 Reads + 4 Edits = 8 built-in calls. +- **Frequency:** Medium. Any non-trivial rename touches 3–10 files. +- **Value per hit:** Saves 4–7 calls and eliminates the partial-update risk of a mid-chain failure. +- **Atomicity:** Serena's rename is all-or-nothing. Built-in chain is not — if Edit 3 of 4 fails, 2 files are updated and 2 are not. + +### 2.2 Symbol moving: 1 call replaces 5+ + +- **What changes:** Moving `_nullable_slice` to another module required 1 Serena call (moved definition + updated imports in both source and target). Built-in equivalent: read function → write to target → edit source (remove + add import) → edit target (add dependency import) = 5+ calls. +- **Frequency:** Low. Module reorganization happens infrequently. +- **Value per hit:** Saves 4+ calls and automates the most error-prone step (getting imports right). +- **Caveat:** The move tool created a circular import (source imports from target, target imports from source). The tool does not detect or prevent this. + +### 2.3 Reference finding: Symbol-accurate vs text-matched + +- **What changes:** Finding references to `CollectStats` with Serena returned 10 semantically categorized results (IMPORT_ELEMENT, REFERENCE_EXPRESSION, NAMED_PARAMETER, etc.) with no false positives from `LoggedCollectStats`. Grep returned 70+ lines including false positives from `LoggedCollectStats` and noise from docstrings/comments. +- **Frequency:** High. "Who uses this?" is one of the most common codebase questions. +- **Value per hit:** Eliminates manual false-positive filtering. However, Serena's output was 64KB (with context snippets) vs Grep's ~5KB — a tradeoff of precision vs verbosity. + +### 2.4 Type hierarchy: 1 call vs 2+ grep-and-parse cycles + +- **What changes:** Getting the full type hierarchy of `BaseCollector` (supertypes: ABC → object; subtypes: Collector → AsyncCollector) required 1 Serena call. Built-in: parse the class definition line for bases + grep for inheritors + recurse = 2–3 calls, and still no access to external library types. +- **Frequency:** Medium. Common when navigating unfamiliar code. +- **Value per hit:** Saves 1–2 calls and returns transitive chains that built-ins can't produce without iteration. + +### 2.5 Structural overview: Hierarchical vs flat + +- **What changes:** `get_symbols_overview(depth=1)` on a 1551-line file returned a structured hierarchy (classes → methods + attributes) in one call. Grep for `^(class |def | def )` returned a flat 60-line list of definitions with line numbers but no attribute information or nesting. +- **Frequency:** High. Opening any unfamiliar file. +- **Value per hit:** Serena shows Protocol fields, class attributes, and method grouping. Grep shows line numbers for navigation but requires more work to understand structure. + +### 2.6 Method body retrieval: Targeted read vs range read + +- **What changes:** Reading `Collector._collect` (330+ lines) required 1 Serena `find_symbol` call using the name path. Built-in: need to know the line range (773–1103) from a prior Grep, then `Read(offset=773, limit=330)`. +- **Frequency:** High. "Show me this method" is routine. +- **Value per hit:** Serena uses stable addressing (name path). Built-in uses ephemeral line numbers that go stale after edits. The practical difference is small when you just read once, but compounds in edit-then-read-again workflows. + +### 2.7 Single-file rename: No meaningful difference + +- **What changes:** Renaming `_nullable_slice` (4 occurrences in one file): Serena rename = 1 call. Edit `replace_all` = 1 call. Identical results. +- **Frequency:** High. +- **Value per hit:** Zero. Both tools handle this equally well. + +### 2.8 Small edits (1–3 lines): Edit is more token-efficient + +- **What changes:** Changing one error message in a 22-line method: Edit sends ~200 chars (old + new string). Serena `replace_symbol_body` sends ~800 chars (entire method body). +- **Frequency:** Very high. +- **Value per hit:** Edit saves ~600 chars of payload per small edit. This reverses for full-body rewrites of 50+ line methods. + +### 2.9 Insertion: Stable address vs text anchor + +- **What changes:** Inserting a new method after `refresh_all_sequence_stats`: Serena used 1 `insert_after_symbol` call with a name path (no line number needed). Edit used 1 Read + 1 Edit with a text anchor (surrounding context for uniqueness). +- **Frequency:** Medium. +- **Value per hit:** Saves 1 Read call. Both produce identical results. + +**Verdict:** Serena's value concentrates in cross-file operations and semantic queries. For single-file text edits, the built-ins are equally capable and often more token-efficient. + +--- + +## 3. Detailed Evidence, Grouped by Capability + +### 3.1 Codebase Understanding + +#### Task 1: Repo overview +Both toolsets use the same approach (`ls`, `find`, directory listing). No Serena advantage here. + +#### Task 2: Structural overview of a large file (`collector.py`, 1551 lines) + +| Step | Serena | Built-in | +|------|--------|----------| +| Call | `get_symbols_overview(depth=1)` | `Grep "^(class \|def \| def )"` | +| Result | Hierarchical: 14 classes, their methods, attributes, and module-level functions | Flat list of 60 class/function definitions with line numbers | +| Output size | ~1.5KB structured JSON | ~3KB text | +| Next step | `find_symbol("Collector/_collect", include_body=True)` — direct | `Read(offset=773, limit=330)` — needs prior knowledge of line range | + +**Serena advantage:** Shows attributes (e.g., `CollectStats.collect_time`, `CollectStats.returns`) that Grep cannot see. Hierarchical nesting makes the file's architecture immediately clear. + +**Built-in advantage:** Line numbers enable direct `Read` calls. Flat output is compact. + +**Verdict:** Serena provides strictly more structural information in one call. The gap widens for files with deeply nested classes or dataclass fields. + +#### Task 3: Retrieve a specific method body + +| Step | Serena | Built-in | +|------|--------|----------| +| Prerequisite | Name path known (`Collector/_collect`) | Line number known from prior Grep (773) | +| Call | `find_symbol(name_path="Collector/_collect", include_body=True)` | `Read(offset=773, limit=330)` | +| Payload sent | ~50 chars (name + path) | ~30 chars (offset + limit) | +| Payload received | Exact method body (~330 lines) | Lines 773–1102 (~330 lines) | +| Correctness | Always exact | Must know/guess the correct limit | + +**Verdict:** Functionally equivalent when line numbers are known. Serena's name-path addressing degrades gracefully across edits; line numbers do not. + +#### Task 4: Find all references to `CollectStats` + +| Metric | Serena `find_referencing_symbols` | Built-in `Grep` | +|--------|----------------------------------|-----------------| +| Calls | 1 | 1 | +| Output size | 64KB (with context snippets) | ~5KB (70 lines) | +| False positives | 0 (excludes `LoggedCollectStats`) | 5+ lines from `LoggedCollectStats` | +| Noise (comments/docs) | Some (docstrings categorized) | Significant (docstrings, comments matched) | +| Semantic categories | Yes (IMPORT, PARAMETER, DECLARATION, REFERENCE) | No | + +**Serena advantage:** Zero false positives. Semantic categorization. Shows import paths and parameter usage separately from code references. + +**Built-in advantage:** 10x smaller output. Faster to scan visually. + +**Verdict:** Serena is more precise but more verbose. For "who uses this in code?" both work; for "rename this safely" Serena's precision is necessary. + +#### Task 5: Type hierarchy of `BaseCollector` + +| Metric | Serena `type_hierarchy` | Built-in (Grep chain) | +|--------|------------------------|----------------------| +| Calls | 1 | 2–3 (grep subclasses, parse superclass, recurse) | +| Result | `ABC → object` (super), `Collector → AsyncCollector` (sub) | Partial — direct sub/supertypes only, no transitive chain | +| External deps | Shows `ABC` from `abc.pyi` | Cannot access | + +**Verdict:** Serena returns complete, transitive hierarchy including external library types in one call. Built-in approach requires iteration and cannot inspect external deps. + +#### Task 6: External dependency symbol lookup + +Serena can read external dependency symbols IF you have the path from a prior tool result (e.g., `` from `type_hierarchy`). Direct search with `search_deps=True` returned empty results for `numpy.array` and `torch.Tensor`. The JetBrains IDE indexing is limited to what the language server has resolved. + +Built-in: Can `Read` site-packages files if you know the path, but discovery is manual. + +**Verdict:** Minor Serena advantage — external symbols are accessible through tool chains but not through direct search. Neither toolset makes this easy. + +### 3.2 Single-File Edits + +#### Task 7a: Small tweak (1-line change in 22-line method) + +| Metric | Edit | Serena `replace_symbol_body` | +|--------|------|------------------------------| +| Prerequisite | 1 Read (6 lines of context) | 1 `find_symbol` (gets full body) | +| Payload sent | ~200 chars (old + new string) | ~800 chars (full method body) | +| Payload received | Success message | "OK" | +| Total payload | ~200 chars edit + ~300 chars read = ~500 | ~800 chars edit + ~800 chars read = ~1600 | + +**Verdict:** Edit is 3x more token-efficient for small tweaks inside methods. + +#### Task 7b: Medium rewrite (~6 line changes in 20-line method) + +| Metric | Edit | Serena `replace_symbol_body` | +|--------|------|------------------------------| +| Payload sent | ~700 chars (old + new, full method) | ~500 chars (new body only) | +| Prerequisite read | ~500 chars | ~500 chars (from prior `find_symbol`) | + +**Verdict:** Roughly equal. For medium rewrites, payloads converge. + +#### Task 7c: Large rewrite (full body of 55+ line method) + +For a full-body rewrite of a 55-line method: +- Edit: old (~55 lines) + new (~55 lines) = ~110 lines sent +- Serena: new body only (~55 lines) sent + +**Verdict:** Serena is ~2x more token-efficient for full-body rewrites. The advantage grows linearly with method size. + +#### Task 8: Insert a new function after an existing one + +| Step | Serena | Built-in | +|------|--------|----------| +| 1 | `find_symbol("refresh_all_sequence_stats")` to confirm target | `Read(offset=250, limit=10)` to find insertion point | +| 2 | `insert_after_symbol(name_path, body)` | `Edit(old_string=anchor, new_string=anchor+new_fn)` | +| Total calls | 2 | 2 | +| Payload sent | New function body only (~300 chars) | Anchor context + new function (~400 chars) | + +**Verdict:** No meaningful difference. Both require 2 calls and produce identical results. + +#### Task 9: Rename a private helper (single-file, 4 occurrences) + +| Metric | Serena `rename` | Edit `replace_all` | +|--------|-----------------|-------------------| +| Calls | 1 | 1 | +| Prerequisites | None | Must have Read the file first | +| Result | All 4 occurrences renamed | All 4 occurrences renamed | + +**Verdict:** Functionally identical. Both are 1 call. Edit's Read-first requirement is usually satisfied from prior exploration. + +### 3.3 Multi-File Changes + +#### Task 10: Cross-file rename (`CollectStatsBase` → `BaseCollectStats`, 4 files, 10 occurrences) + +| Step | Serena | Built-in | +|------|--------|----------| +| Find references | Automatic | 1 Grep call | +| Read files | Not needed | 3 Read calls (Edit's prerequisite) | +| Apply edits | 1 rename call (atomic) | 4 Edit calls (one per file) | +| Verify | Return: "Success" | Manual (4 success messages) | +| **Total calls** | **1** | **8** (1 grep + 3 reads + 4 edits) | + +**Verdict:** Serena converts an 8-call manual pipeline into 1 atomic operation. This is the single largest efficiency gain observed. + +#### Task 11: Move symbol to another module + +Serena's `move` tool: +1. Moved `_nullable_slice` from `collector.py` to `converter.py` +2. Added import in source file: `from tianshou.data.utils.converter import _nullable_slice` +3. Added dependency import in target: `from tianshou.data.collector import _TArrLike` +4. Removed definition from source + +Built-in equivalent: Read function body → Write to target → Edit source (remove definition + add import) → Edit target (add dependency import) = 5+ calls. + +**Issue:** The move created a circular import (source ↔ target). Serena does not detect or prevent this. + +**Verdict:** Serena automates the most tedious part (import management) but doesn't guard against circular dependencies. Saves 4+ calls at the cost of needing manual circular-import review. + +#### Task 12: Move file (`segtree.py` to parent directory) + +Serena's `move` tool: +1. Moved the file +2. Updated the one direct import in `__init__.py` (`tianshou.data.utils.segtree` → `tianshou.data.segtree`) +3. Other files (`prio.py`, tests) imported via re-export and needed no changes + +Built-in equivalent: `git mv` + grep for old import path + edit each file = 3+ calls. + +**Verdict:** Serena saves 1–2 calls and automatically discovers which imports need updating. + +#### Task 12 (safe delete) + +Serena's `safe_delete` correctly refused to delete `_HACKY_create_info_batch` because it has a usage at line 730. The `propagate=true` mode (delete symbol + all call sites) failed for all tested symbols in this codebase. + +**Verdict:** The usage-check is valuable (saves you from deleting a used symbol). The propagation feature was non-functional for the tested Python symbols. + +#### Task 13: Inline + +Serena's `inline_symbol` failed for all tested symbols (`_nullable_slice`, `BaseCollector/env_num`). The tool appears to have limited Python support for inlining. + +**Verdict:** No successful inline demonstrated. Built-in manual inlining remains the only option. + +### 3.4 Reliability and Correctness + +#### Task 14: Scope precision + +Serena distinguishes `BaseCollector/_collect`, `Collector/_collect`, and `AsyncCollector/_collect` by name path. Grep for `def _collect` matches all three — manual filtering by class is required. + +**Verdict:** Serena's name-path addressing eliminates ambiguity that text search cannot resolve. + +#### Task 15: Atomicity + +Serena's cross-file rename is atomic: 4 files updated in 1 call, all-or-nothing. Built-in: 4 separate Edit calls — if call 3 fails, 2 files are updated and 2 are not. + +**Verdict:** Serena provides atomicity for cross-file operations. Built-in chains are inherently non-atomic. + +#### Task 16: Success signals + +Both return clear success/failure indicators. No meaningful difference. + +**Verdict:** Equal. + +### 3.5 Workflow Effects + +#### Task 17: Chain three edits in one file + +Edit with text matching: 3 sequential calls, no re-reads needed between them. Text anchors are immune to line-number shifts from prior edits. + +Serena `replace_symbol_body`: 3 sequential calls, no re-reads needed. Name-path addressing is also immune to line-number shifts. + +**Verdict:** No meaningful difference for chained single-file edits. + +#### Task 18: Multi-step exploration across edits + +Serena's name-path results from exploration remain valid after edits. Built-in line numbers go stale, but Edit uses text matching (not line numbers), so the practical impact is limited to `Read` calls that need updated offsets. + +**Verdict:** Minor Serena advantage. Name-path stability eliminates the need to re-scan after edits. + +### 3.6 Non-Interesting Tasks + +#### Task 19: Read non-code file + +Serena tools don't apply. `Read` is the correct tool. + +#### Task 20: Free-text pattern search + +Searching for `FIXME|HACK|TODO` across the codebase is a text search. Serena's semantic tools don't target this. `Grep` is the correct tool. + +**Verdict:** These tasks are firmly built-in territory. They represent an estimated 20–30% of daily coding work (reading configs, searching for strings, shell operations, git workflows). + +--- + +## 4. Token-Efficiency Analysis + +### Payload differences across edit sizes + +| Edit type | Edit payload | Serena payload | Winner | +|-----------|-------------|---------------|--------| +| 1-line tweak in 22-line method | ~200 chars | ~800 chars | Edit (4x) | +| 6-line change in 20-line method | ~700 chars | ~500 chars | Roughly equal | +| Full rewrite of 55-line method | ~2200 chars | ~1100 chars | Serena (2x) | +| Full rewrite of 330-line method | ~13,000 chars | ~6,500 chars | Serena (2x) | + +### Forced reads + +- Edit requires reading a file before editing it. This adds ~300–2000 chars per file. +- Serena does not require reading before editing (name-path addressing). +- For single-file edits where you already read the file, this is neutral. +- For cross-file operations on files you haven't read, Serena saves 3–4 forced reads. + +### Stable vs ephemeral addressing + +- Serena: name paths (`Collector/_collect`) are stable across edits. Results from exploration remain valid. +- Built-in: line numbers are ephemeral. `Read` results go stale after edits. `Edit` uses text matching, which is stable. +- Practical impact: Low for one-shot edits, medium for edit-then-read-again workflows. + +**Verdict:** Edit wins for small tweaks (4x more token-efficient). Serena wins for full-body rewrites (2x more efficient) and cross-file operations (eliminates forced reads). The crossover point is approximately 50% of the method body changing — below that, Edit is more efficient; above that, Serena is. + +--- + +## 5. Reliability and Correctness (Under Correct Use) + +### Precision of matching + +- Serena: Symbol-accurate. `find_referencing_symbols(CollectStats)` excludes `LoggedCollectStats`. No false positives observed. +- Grep: Text-matched. `CollectStats` matches `LoggedCollectStats`, `CollectStatsBase`, and docstring references. Requires manual filtering. +- Edit: Text-matched. `replace_all` replaces exact string matches. For unique symbol names, this is reliable. For common strings, it can over-match. + +### Scope disambiguation + +- Serena: `Collector/_collect` vs `AsyncCollector/_collect` — correctly distinguished by class-scoped name path. +- Built-in: `def _collect` matches all implementations. Must manually verify class context. + +### Atomicity + +- Serena cross-file operations: Atomic. Single call, all-or-nothing. +- Built-in multi-file chains: Non-atomic. Partial state possible if one call fails. + +### External dependency lookup + +- Serena: Can read external stubs (e.g., `abc.pyi`) through paths returned by other tools. Direct search (`search_deps=True`) returned empty for `torch.Tensor` and `numpy.array`. Limited to what JetBrains has indexed. +- Built-in: Can `Read` site-packages files if path is known. No semantic indexing. + +**Verdict:** Serena provides strictly more precise semantic matching and atomic cross-file operations. External dependency lookup is limited in both toolsets. + +--- + +## 6. Workflow Effects Across a Session + +### Where advantages compound + +1. **Explore → edit → re-explore cycle:** Serena's name-path results survive edits. In a long session making multiple changes, this saves re-scanning after each edit. The built-in's text matching also survives edits (Edit uses text, not line numbers), so the practical gap is smaller than it appears. + +2. **Cross-file refactoring chains:** Rename a class, then move it, then update all references — each Serena call is atomic and builds on the previous result. With built-ins, each step requires finding all sites, reading files, and editing — the manual equivalent of what Serena automates. + +### Where advantages diminish + +1. **Repeated small edits in one file:** Edit's text matching is equally stable and more token-efficient for small changes. No Serena advantage. + +2. **Exploration without editing:** Both toolsets provide usable results. Serena's are more structured but more verbose. + +3. **Non-Python files:** Serena's JetBrains backend provides no value for config files, shell scripts, markdown, or notebooks. + +**Verdict:** Serena's advantages compound in multi-step cross-file refactoring sessions. They do not compound for single-file iterative editing or non-code work. + +--- + +## 7. Unique Capabilities + +1. **Atomic cross-file rename/move** — No built-in equivalent. The closest manual process is a grep-find-edit chain that is non-atomic and error-prone. Frequency: medium. Impact: high (eliminates partial-update risk). + +2. **Semantic reference finding with categorization** — `find_referencing_symbols` returns zero-false-positive results categorized by usage type (import, parameter, declaration, reference). Built-in Grep cannot distinguish these. Frequency: high. Impact: medium (saves manual filtering). + +3. **Type hierarchy traversal** — Returns transitive super/subtype chains including external library types in one call. Built-in requires iteration and cannot reach external deps. Frequency: medium. Impact: medium. + +4. **Symbol-scoped body retrieval** — Read a specific method by name path without reading the surrounding file. Built-in `Read` requires line-range knowledge. Frequency: high. Impact: low (both require 1 call, difference is stable vs ephemeral addressing). + +**Verdict:** Four unique capabilities, all semantic-code operations. The most impactful is atomic cross-file refactoring. None of these have practical built-in equivalents. + +--- + +## 8. Tasks Outside Serena's Scope (Built-In Only) + +| Task | Tool | Frequency | Share of daily work | +|------|------|-----------|-------------------| +| Read config/TOML/yaml files | `Read` | High | | +| Free-text search (log strings, TODOs, URLs) | `Grep` | High | | +| File discovery by name pattern | `Glob` | Medium | | +| Shell commands (git, pip, pytest) | `Bash` | High | | +| Write new files from scratch | `Write` | Medium | | +| Read images, notebooks, PDFs | `Read` | Low | | + +Estimated share of daily work covered by built-in-only tasks: **20–30%**. The remaining 70–80% involves reading, editing, and navigating code where Serena's semantic tools are applicable. + +**Verdict:** Serena covers the code-editing and code-navigation portions of a session. Config reading, text search, and shell operations remain built-in territory. + +--- + +## 9. Practical Usage Rule + +**Use Serena for:** Any cross-file refactoring (rename, move), any "who uses this?" query, any type-hierarchy navigation, any full-method-body replacement, and any situation where you need symbol-accurate results without false positives. + +**Use built-ins for:** Small edits (1–3 lines) inside methods, free-text search, reading non-code files, file discovery, and shell operations. + +**Hybrid pattern (most efficient):** Use Serena to explore (overview, find symbol, find references) and Edit for small targeted changes. Use Serena for any cross-file refactoring. Use `Read`/`Grep`/`Glob` for non-code tasks and text search. This combination captures the strengths of both toolsets. + +**Verdict:** The optimal workflow uses Serena's semantic tools for code navigation and cross-file refactoring, and built-in Edit for small single-file changes. The two toolsets are complementary — Serena handles the structured code operations, built-ins handle the text and system operations. + +--- + +## Appendix: Call Count Summary + +| Task | Serena calls | Built-in calls | Delta | +|------|-------------|----------------|-------| +| Structural overview (1 file) | 1 | 1 | 0 | +| Method body retrieval | 1 | 1 | 0 | +| Find references (1 symbol) | 1 | 1 | 0 (but Serena has 0 false positives vs Grep's 5+) | +| Type hierarchy | 1 | 2–3 | −1 to −2 | +| Small edit (1 line in 22-line method) | 1 (+1 read) | 1 (+1 read) | 0 | +| Medium edit (6 lines in 20-line method) | 1 | 1 | 0 | +| Insert new method | 1 (+1 confirm) | 1 (+1 read) | 0 | +| Single-file rename (4 occurrences) | 1 | 1 | 0 | +| **Cross-file rename (4 files, 10 occurrences)** | **1** | **8** | **−7** | +| **Move symbol to another module** | **1** | **5+** | **−4+** | +| **Move file + update imports** | **1** | **3+** | **−2+** | +| Safe delete (usage check) | 1 | 1 grep | 0 | + +Largest single-task delta: cross-file rename saves 7 calls and provides atomicity. diff --git a/docs/04-evaluation/030_results/050_junie_plugin_on_tianshou.md b/docs/04-evaluation/030_results/050_junie_plugin_on_tianshou.md new file mode 100644 index 00000000..cfafc462 --- /dev/null +++ b/docs/04-evaluation/030_results/050_junie_plugin_on_tianshou.md @@ -0,0 +1,345 @@ +:::{admonition} Evaluation Result +:class: note +**Generated by**: Claude Opus 4.6 (coding AI agent in JetBrains Junie Plugin) +**Codebase:** [Tianshou](https://github.com/thu-ml/tianshou) — a Python reinforcement learning library (~26K lines, 43 source files) +**Date**: 2026-04-17 +::: + +# JetBrains Junie Plugin (Opus 4.6) + +> Serena gives me what my built-in tools can't — the ability to move a function between modules with all imports updated atomically, trace a class hierarchy into dependencies, and safely delete symbols with usage guards — and I'd ask my owner to add it for the move-refactoring and semantic navigation capabilities alone. + +--- + +## 1. Headline: What Serena Changes + +Serena adds three categories of capability on top of built-in tools: + +**(a) Tasks where Serena adds capability:** +- **Move symbol between modules** (with automatic import updates) — no built-in equivalent. +- **Move file/package** (with automatic import updates) — no built-in equivalent. +- **Type hierarchy traversal** — transitive supertype/subtype chains in 1 call, including into external dependencies. No built-in equivalent. +- **External dependency symbol lookup** — search and inspect symbols in installed packages (site-packages) without manual path discovery. Built-ins cannot do this. +- **Safe delete with usage checking** — refuses deletion if usages exist, reports exact locations. Built-ins require a manual search-then-decide workflow. +- **Propagated delete** — delete a symbol and automatically remove all call sites. No built-in equivalent. +- **Inline symbol** — replace all call sites with the function body. No built-in equivalent. +- **Scope-precise symbol targeting** — name paths like `AsyncCollector/_collect` disambiguate overrides that text search cannot distinguish. + +**(b) Tasks where Serena applies but offers no meaningful improvement:** +- **Cross-file rename** — both Serena (`rename`) and built-in (`rename_element`) perform semantic, cross-file renames in 1 call. Functionally equivalent. +- **Single-file rename** — same as above; both are 1-call semantic renames. +- **Structural overview of a single file** — both toolsets produce comparable results in 1 call. +- **Small edits (1-3 lines)** — Serena's `replace_symbol_body` must send the entire method body; built-in `search_replace` sends only the changed line. Built-in is more token-efficient here. + +**(c) Tasks outside Serena's scope (built-in only):** +- Reading non-code files (configs, docs, changelogs) +- Free-text search across the repo (log strings, URLs, magic constants) +- Terminal commands, test execution, git operations +- Creating new files from scratch +- Repository-level directory listing + +**Verdict:** +Serena's primary contribution is move-refactoring (symbol and file moves with import updates), semantic code navigation (type hierarchy, dependency lookup), and safe/propagated delete — capabilities with no built-in equivalent. + +--- + +## 2. Added Value and Differences by Area + +- **Move symbol/file with import updates (positive).** Frequency: occasional in refactoring work. Value per hit: saves ~7 calls and eliminates manual import tracking. Moving `get_stddev_from_dist` between modules: 1 Serena call vs. ~7 built-in calls. No built-in equivalent. + +- **Type hierarchy and reference finding (positive).** Frequency: a few times per exploration session. Value per hit: saves 2-4 search-and-read cycles and provides information (transitive supertypes, external dependency chains) that built-ins cannot produce at all. `BaseCollector`'s full hierarchy (ABC→object up, Collector→AsyncCollector down) returned in 1 call. + +- **Targeted symbol retrieval by name path (positive).** Frequency: many times per session. Value per hit: saves 1 prerequisite read (no line-number lookup needed). `find_symbol` with `include_body=true` returns exact method body in 1 call vs. 2 calls (structure + open). + +- **Safe delete with usage guard (positive).** Frequency: occasional. Value per hit: eliminates risk of orphaned references. Serena refused to delete `_dict_of_arr_to_arr_of_dicts` and reported 2 exact usages. Built-in: requires manual search first. + +- **Cross-file rename (neutral).** Frequency: several times per session. Value per hit: zero — both Serena's `rename` and built-in `rename_element` perform semantic cross-file renames in 1 call with equivalent results. + +- **Single-file edits (neutral to slightly negative for small edits).** Frequency: very high. Value per hit: for small edits, Serena's `replace_symbol_body` sends more tokens (entire method body) than `search_replace` (just the changed lines). For full method rewrites, comparable. For inserts, Serena's `insert_after_symbol` saves 1 prerequisite read. + +- **Structural overview (neutral).** Frequency: a few times per session. Value per hit: both toolsets return comparable information in 1 call. + +**Verdict:** Serena's value concentrates in move-refactoring and semantic navigation; rename is matched by the built-in `rename_element`. + +--- + +## 3. Detailed Evidence, Grouped by Capability + +### 3.1 Structural Overview (Task 2) + +**Attempted:** Get structural overview of `collector.py` (1552 lines, 15+ classes). + +| Axis | Serena (`get_symbols_overview`) | Built-in (`get_file_structure`) | +|------|------|------| +| Calls | 1 | 1 | +| Output | JSON tree: class names, method names, field names | Flat list: class/method names with line ranges and signatures | +| Unique info | Field/attribute names | Line numbers, full parameter signatures | +| Follow-up to read a method | `find_symbol` by name path (1 call) | `open` at line number (1 call) | + +**Verdict:** Functionally equivalent; each includes information the other omits. No meaningful delta. + +### 3.2 Targeted Symbol Retrieval (Task 3) + +**Attempted:** Retrieve body of `CollectStats/refresh_all_sequence_stats` without reading surrounding file. + +| Axis | Serena | Built-in | +|------|------|------| +| Calls | 1 (`find_symbol` with `include_body`) | 2 (`get_file_structure` + `open`) | +| Prerequisite | None (name path is stable) | Must know line number | +| Output payload | 4 lines (exact method body) | 100 lines (open window) | + +**Verdict:** Serena saves 1 call and returns a smaller, more precise payload. Minor but consistent advantage. + +### 3.3 Reference Finding (Task 4) + +**Attempted:** Find all references to `CollectStats` across the codebase. + +| Axis | Serena (`find_referencing_symbols`) | Built-in (`search_project`) | +|------|------|------| +| Calls | 1 | 1 | +| Output | Summarized narrative: grouped by file, annotated with context (imports, instantiations, type hints) | Raw list: 100+ text matches, includes docs, comments, strings | +| Precision | Code references only | All text mentions | +| Recall | Code references only | Everything including non-code | + +**Verdict:** Serena provides higher precision (code-only references with semantic context). Built-in provides higher recall (includes docs, comments). Different tools for different questions. + +### 3.4 Type Hierarchy (Task 5) + +**Attempted:** Full type hierarchy of `BaseCollector` — supertypes and subtypes, transitively. + +| Axis | Serena (`type_hierarchy`) | Built-in | +|------|------|------| +| Calls | 1 | 3+ (grep for class declarations, read each, trace manually) | +| Result | `BaseCollector → ABC → object` (up), `→ Collector → AsyncCollector` (down) | Partial: can find direct subclasses by grep, but supertypes of supertypes require reading external files | +| External deps | Included (ABC from `abc` module, object from `builtins`) | Not accessible | + +**Verdict:** Unique capability. No practical built-in equivalent for transitive hierarchy, especially into dependencies. + +### 3.5 External Dependency Lookup (Task 6) + +**Attempted:** Find `Distribution` class from `torch.distributions`. + +| Axis | Serena (`find_symbol` with `search_deps`) | Built-in | +|------|------|------| +| Calls | 1 | Not possible without manual site-packages navigation | +| Result | Found 41 `Distribution` classes across all installed packages with ext-identifiers | N/A | +| Follow-up | Can use ext-identifier to read body/info | Would need to find virtualenv path, navigate to package, read file | + +**Verdict:** Unique capability. Built-ins have no access to dependency source code without manual path discovery. + +### 3.6 Small Edit — 1 Line Change (Task 7a) + +**Attempted:** Change error message in `_validate_buffer` (21-line method). + +| Axis | Serena | Built-in | +|------|------|------| +| Calls | 2 (`find_symbol` + `replace_symbol_body`) | 1 (`search_replace`) | +| Input payload | ~21 lines (full method body) | ~1 line (search) + ~1 line (replace) | +| Prerequisite reads | 1 (find_symbol to get current body) | 0 (if search string is known) | + +**Verdict:** Built-in is more efficient for small, targeted edits. Serena's method-granularity addressing forces sending the entire body. + +### 3.7 Medium Rewrite — ~20 Lines (Task 7b) + +**Attempted:** Rewrite `update_at_step_batch` (20 lines), changing variable names and adding logging. + +| Axis | Serena | Built-in | +|------|------|------| +| Calls | 2 (`find_symbol` + `replace_symbol_body`) | 1 (`search_replace` with old body → new body) | +| Input payload | ~20 lines (new body) | ~40 lines (old + new body) | +| Prerequisite reads | 1 (find_symbol) | 1 (open to see current code) | + +**Verdict:** Comparable. Serena sends less payload (new body only vs. old+new), but requires a prerequisite find_symbol call. + +### 3.8 Insert New Method (Task 8) + +**Attempted:** Insert `summary_string` method after `refresh_all_sequence_stats` in `CollectStats`. + +| Axis | Serena (`insert_after_symbol`) | Built-in (`search_replace`) | +|------|------|------| +| Calls | 1 | 1-2 (need to find anchor text, then insert) | +| Addressing | By name path (stable) | By text anchor or line number (fragile) | +| Prerequisite | None | May need `open` to find insertion point | + +**Verdict:** Serena's stable addressing is a minor advantage — eliminates the need to find an anchor. + +### 3.9 Single-File Rename (Task 9) + +**Attempted:** Rename `_nullable_slice` → `_slice_if_not_none` (5 occurrences in 1 file). + +| Axis | Serena (`rename`) | Built-in (`rename_element`) | +|------|------|------| +| Calls | 1 | 1 | +| Scope | Semantic (only code references) | Semantic (only code references) | + +**Verdict:** Functionally equivalent. Both perform semantic renames in 1 call. + +### 3.10 Cross-File Rename (Task 10) + +**Attempted:** Rename `CollectStatsBase` → `CollectStatsFoundation` (used in 4 files, 10 occurrences including imports and `__all__`). + +| Axis | Serena (`rename`) | Built-in (`rename_element`) | +|------|------|------| +| Calls | 1 | 1 | +| Atomicity | All-or-nothing | All-or-nothing | +| Import handling | Automatic | Automatic | +| Result | 4 files, 10 replacements | 4 files, 10 replacements | + +**Verdict:** No delta. Both toolsets provide atomic, semantic cross-file rename in a single call. + +### 3.11 Move Symbol Between Modules (Task 11) + +**Attempted:** Move `get_stddev_from_dist` from `collector.py` to `stats.py`. + +| Axis | Serena (`move`) | Built-in | +|------|------|------| +| Calls | 1 | ~7 (read source, copy to target, delete from source, find imports, update each import, verify) | +| Files modified | 3 (source, target, test file) | Same 3, but manually | +| Import updates | Automatic | Manual | +| Atomicity | Atomic | Non-atomic | + +**Verdict:** Unique capability at this level of automation. No practical built-in equivalent at comparable effort. This is now the highest-value delta in the evaluation. + +### 3.12 Move File (Task 12) + +**Attempted:** Move `segtree.py` from `tianshou/data/utils/` to `tianshou/utils/`. + +| Axis | Serena (`move`) | Built-in | +|------|------|------| +| Calls | 1 | 3+ (bash mv, find imports, update each) | +| Import updates | Automatic (updated `__init__.py`) | Manual | + +**Verdict:** Serena advantage — single call with automatic import updates. + +### 3.13 Safe Delete (Task 13) + +**Attempted:** Delete `_dict_of_arr_to_arr_of_dicts` (has 2 usages). + +| Axis | Serena (`safe_delete`) | Built-in | +|------|------|------| +| Calls | 1 (refused, reported usages) | 2 (search_project + manual decision) | +| Safety | Refuses deletion, reports exact usages with file:line and enclosing function | Must manually verify search results | + +**Verdict:** Serena provides a safety guard with precise diagnostics. Minor but useful advantage. + +### 3.14 Scope Precision (Task 14) + +**Attempted:** Target `AsyncCollector/_collect` specifically (3 classes define `_collect`). + +Serena's name path `AsyncCollector/_collect` unambiguously selects the override. A text search for `_collect` matches all three definitions plus dozens of call sites. Built-in `get_file_structure` can disambiguate by line number, but that requires a prerequisite read and the line number goes stale after edits. + +**Verdict:** Serena's name-path addressing provides persistent, unambiguous symbol targeting. Meaningful advantage when multiple overrides exist. + +### 3.15 Chained Edits (Task 17) + +**Attempted:** Three sequential `replace_symbol_body` calls on different methods in the same file. + +All three succeeded without re-reading the file. Name paths remained stable across edits. Built-in `search_replace` also chains without re-reading (text anchors remain valid if edits are in different methods), but line numbers from `open` go stale after each edit. + +**Verdict:** Both toolsets chain well. Serena's name-path stability is a minor advantage over line-number-based addressing. + +--- + +## 4. Token-Efficiency Analysis + +| Edit size | Serena payload | Built-in payload | Winner | +|-----------|---------------|-----------------|--------| +| Small (1-3 lines in 20-line method) | ~20 lines (full body) | ~2 lines (search+replace) | Built-in (10x less) | +| Medium (rewrite ~20 lines) | ~20 lines (new body) + prerequisite find | ~40 lines (old+new) | Comparable | +| Large (rewrite 50+ lines) | ~50+ lines (new body) | ~100+ lines (old+new) | Serena (2x less, no old body needed) | +| Insert | ~N lines (new code only) | ~N lines + anchor context | Comparable | +| Cross-file rename | ~1 line (name + new name) | ~1 line (name + new name) | Tie (both semantic) | +| Move symbol | ~1 line (source + target) | ~7 calls with full bodies | Serena (>>10x less) | + +**Forced reads:** Serena's `replace_symbol_body` requires a `find_symbol` call to get the current body before editing. Built-in `search_replace` needs no prerequisite if the search text is known, but often requires `open` to discover it. + +**Stable vs. ephemeral addressing:** Serena's name paths survive edits; built-in line numbers do not. This matters in multi-edit sessions — Serena never needs to re-read for addressing purposes, while built-in may need to re-open after edits that shift line numbers. + +**Verdict:** For single-file small edits, built-ins are more token-efficient. For move-refactoring, Serena is substantially more efficient. For rename, both are equivalent. The crossover point for edits is roughly at method-level rewrites. + +--- + +## 5. Reliability & Correctness (Under Correct Use) + +- **Precision of matching:** Serena matches by semantic identity (name path in the symbol tree). Built-in `search_replace` matches by text (can over-match). However, built-in `rename_element` also matches semantically. For rename operations, precision is equivalent. + +- **Scope disambiguation:** Serena's `AsyncCollector/_collect` is unambiguous. Built-in text search for `def _collect` returns 3 matches in collector.py alone. Disambiguation requires line numbers or surrounding context. Built-in `rename_element` also requires a file path and line number to disambiguate. + +- **Atomicity:** Both Serena's and built-in's rename operations are atomic. Serena's move operations are also atomic; built-ins have no equivalent move, so the comparison is moot. For sequential text edits (`search_replace`), partial failure is possible. + +- **Semantic queries vs. text search:** Serena's `find_referencing_symbols` returns only code references with semantic context. Built-in `search_project` returns all text matches. Neither is strictly better — they answer different questions. + +- **External dependency lookup:** Serena can search and inspect symbols in installed packages via `search_deps=true`. Built-ins have no access to dependency source without manual virtualenv/site-packages navigation. This depends on the IDE having indexed the project's interpreter. + +**Verdict:** For rename, correctness guarantees are equivalent between toolsets. Serena's unique correctness advantage is in move-refactoring atomicity and external dependency access. + +--- + +## 6. Workflow Effects Across a Session + +- **Multi-edit stability:** Serena's name-path addressing remains valid across edits within a session. Built-in line numbers go stale after any edit that changes line counts. In a session with 10+ edits to the same file, Serena eliminates ~5-10 re-read calls that built-ins would need to refresh line numbers. + +- **Exploration → edit transition:** After exploring with Serena (find_symbol, type_hierarchy), the same name paths can be used directly for edits (replace_symbol_body, insert_after_symbol). Built-in exploration (get_file_structure) produces line numbers that must be used immediately before they go stale. + +- **Cross-file refactoring chains:** A rename is equivalent on both sides (1 call each). A move followed by a delete is 2 Serena calls vs. ~10+ built-in calls. The advantage compounds for move-heavy refactoring but not for rename-heavy refactoring. + +- **Session overhead:** Serena requires project activation (1 call at session start). The project can deactivate unexpectedly (observed once during this evaluation), requiring reactivation. This is a minor but real friction cost. + +**Verdict:** Serena's advantages compound for move-refactoring and exploration-heavy sessions; for rename-heavy sessions, both toolsets are equivalent. + +--- + +## 7. Unique Capabilities (No Built-In Equivalent) + +| Capability | Frequency | Impact per use | +|-----------|-----------|---------------| +| Move symbol between modules (with import updates) | Occasional | Saves ~7 calls, eliminates manual import tracking | +| Move file with import updates | Occasional | Saves ~3 calls | +| Transitive type hierarchy | A few times per exploration session | Provides information unavailable to built-ins | +| External dependency symbol lookup | Occasional | Provides information unavailable to built-ins | +| Safe delete with usage guard | Occasional | Prevents accidental breakage | +| Propagated delete (delete + remove call sites) | Rare | Saves multiple manual edits | +| Inline symbol | Rare | Saves multiple manual edits | +| Scope-precise symbol targeting by name path | Continuous (every symbol interaction) | Eliminates disambiguation overhead | + +**Verdict:** Serena provides 5-6 capabilities with no practical built-in equivalent, concentrated in move-refactoring, semantic navigation, and safe/propagated delete. + +--- + +## 8. Tasks Outside Serena's Scope (Built-In Only) + +| Task | Share of typical session | +|------|------------------------| +| Reading/editing non-code files (configs, docs, changelogs, notebooks) | ~15-20% | +| Free-text search (log strings, URLs, magic constants) | ~10% | +| Terminal commands (git, test runners, build tools) | ~15-20% | +| Creating new files from scratch | ~5-10% | +| Directory listing and repo navigation | ~5% | +| Running and debugging applications | ~10-15% | + +Estimated share of session where Serena's augmentation applies: **~35-50%** (code navigation, symbol editing, refactoring). The remainder is built-in-only territory. + +**Verdict:** Serena augments roughly a third to half of typical session work; the rest is inherently outside its scope and handled by built-ins. + +--- + +## 9. Practical Usage Rule + +| Task type | Use | +|-----------|-----| +| Cross-file rename | Either (both provide semantic rename in 1 call) | +| Move symbol/file with import updates | Serena (unique capability) | +| Safe/propagated delete | Serena (unique capability) | +| Inline symbol | Serena (unique capability) | +| Type hierarchy, dependency lookup | Serena (unique capability) | +| Find code references to a symbol | Serena (`find_referencing_symbols`) | +| Find text mentions anywhere | Built-in (`search_project` / Grep) | +| Small edit (1-3 lines) | Built-in (`search_replace`) | +| Full method rewrite | Either (comparable) | +| Insert at structural location | Serena (`insert_after_symbol`) if name path known; built-in otherwise | +| Read non-code files | Built-in (`open` / Read) | +| Terminal operations | Built-in (bash) | +| New file creation | Built-in (`create`) | +| Chained edits in one file | Either; Serena if line-number staleness is a concern | + +**Verdict:** Use Serena for move-refactoring, semantic navigation, and safe delete; use either toolset for rename; use built-ins for text-level edits, non-code files, and terminal operations. diff --git a/docs/autogen_docs.py b/docs/autogen_docs.py index 2043017c..43976fd9 100644 --- a/docs/autogen_docs.py +++ b/docs/autogen_docs.py @@ -207,6 +207,15 @@ def autogen_about_intro_features(): # remove callouts about_text = re.subn(r"^> \[!\w+\]\s+(^>.*?$)+", "", about_text, flags=re.MULTILINE)[0] + # replace "Quick Demo" with "Video Introduction", removing the short video + about_text = about_text.replace("# Quick Demo", "# Video Introduction") + about_text = re.subn(r"^https://github.com/user-attachments/.*?$", "", about_text, flags=re.MULTILINE)[0] + about_text = about_text.replace(":tv: Longer video:", "Watch our video:") + + # remove emojis (e.g. :tv:) + about_text = re.subn(r":\w+:\s+", "", about_text)[0] + + f.write(f"{about_text}\n\n") jetbrains_marketplace_link = ('```{raw} html\n' diff --git a/flake.nix b/flake.nix index e172a186..9550e4ff 100644 --- a/flake.nix +++ b/flake.nix @@ -100,6 +100,7 @@ } '' mkdir -p $out/bin ln -s ${packages.serena-env}/bin/serena $out/bin/serena + ln -s ${packages.serena-env}/bin/serena-hooks $out/bin/serena-hooks ''; default = packages.serena; }; diff --git a/pyproject.toml b/pyproject.toml index e09bf469..df90518d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,13 +88,11 @@ dev = [ "sphinx_book_theme==1.1.4", "sphinx-toolbox==3.7.0", "jupyter-book==1.0.4.post1", - "nbsphinx==0.9.7", "pyinstrument==5.1.1", "pytest-timeout==2.4.0", # Transitive dev deps pinned for security (dependabot alerts). # Exact pins because uvx installs from git, ignoring the lock file. "tornado==6.5.5", - "nbconvert==7.17.0", "wheel==0.46.3", "pyasn1==0.6.3", "PyJWT==2.12.0", diff --git a/scripts/demo_run_tools.py b/scripts/demo_run_tools.py index d382dc32..b0ed063b 100644 --- a/scripts/demo_run_tools.py +++ b/scripts/demo_run_tools.py @@ -7,8 +7,6 @@ import json from pathlib import Path from pprint import pprint -from toon_format import encode - from serena.agent import SerenaAgent from serena.config.serena_config import SerenaConfig from serena.constants import REPO_ROOT @@ -46,5 +44,5 @@ if __name__ == "__main__": # keep_definition=True, ) ) - pprint(encode(json.loads(result))) + pprint(json.loads(result)) # input("Press Enter to continue...") diff --git a/src/serena/agent.py b/src/serena/agent.py index efe4fc18..e3800b97 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -8,8 +8,10 @@ import os import platform import signal import threading +from collections import defaultdict from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager +from dataclasses import dataclass from datetime import datetime from enum import Enum from logging import Logger @@ -18,6 +20,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar import requests import webview from sensai.util import logging +from sensai.util.helper import mark_used from sensai.util.logging import LogTime from sensai.util.string import dict_string @@ -198,52 +201,125 @@ class ToolSet: class ActiveModes: + _mode_instances: dict[str, SerenaAgentMode] = {} + def __init__(self) -> None: - self._base_modes: Sequence[str] | None = None - self._default_modes: Sequence[str] | None = None - self._active_mode_names: Sequence[str] | None = [] - self._active_modes: Sequence[SerenaAgentMode] | None = [] + self._configured_base_modes: Sequence[str] | None = None + self._configured_default_modes: Sequence[str] | None = None + self._active_mode_names: Sequence[str] = [] def apply(self, mode_selection: ModeSelectionDefinition) -> None: - # invalidate active modes - self._active_mode_names = None - self._active_modes = None - # apply overrides log.debug("Applying mode selection: default_modes=%s, base_modes=%s", mode_selection.default_modes, mode_selection.base_modes) if mode_selection.base_modes is not None: - self._base_modes = mode_selection.base_modes + self._configured_base_modes = mode_selection.base_modes if mode_selection.default_modes is not None: - self._default_modes = mode_selection.default_modes - log.debug("Current mode selection: base_modes=%s, default_modes=%s", self._base_modes, self._default_modes) + self._configured_default_modes = mode_selection.default_modes + log.debug("Current mode selection: base_modes=%s, default_modes=%s", self._configured_base_modes, self._configured_default_modes) + + self._active_mode_names = sorted(set(self._configured_base_modes or []) | set(self._configured_default_modes or [])) def get_mode_names(self) -> Sequence[str]: - if self._active_mode_names is not None: - return self._active_mode_names - active_mode_names: set[str] = set() - if self._base_modes is not None: - active_mode_names.update(self._base_modes) - if self._default_modes is not None: - active_mode_names.update(self._default_modes) - self._active_mode_names = sorted(active_mode_names) - log.info("Active modes: %s", self._active_mode_names) return self._active_mode_names + @classmethod + def get_mode_instance(cls, mode_name: str) -> SerenaAgentMode: + if mode_name not in cls._mode_instances: + cls._mode_instances[mode_name] = SerenaAgentMode.load(mode_name) + return cls._mode_instances[mode_name] + def get_modes(self) -> Sequence[SerenaAgentMode]: - if self._active_modes is not None: - return self._active_modes - self._active_modes = [] - for mode_name in self.get_mode_names(): - mode = SerenaAgentMode.load(mode_name) - self._active_modes.append(mode) - return self._active_modes + return [self.get_mode_instance(mode_name) for mode_name in self._active_mode_names] - # TODO: apply caching like in get_modes def get_default_modes(self) -> Sequence[SerenaAgentMode]: - return [SerenaAgentMode.load(mode_name) for mode_name in self._default_modes or []] + return [self.get_mode_instance(mode_name) for mode_name in self._configured_default_modes or []] def get_base_modes(self) -> Sequence[SerenaAgentMode]: - return [SerenaAgentMode.load(mode_name) for mode_name in self._base_modes or []] + return [self.get_mode_instance(mode_name) for mode_name in self._configured_base_modes or []] + + +class ProjectPromptProvisionStatus: + """ + Manages the status of the provision of project-specific prompts + """ + + @dataclass + class SessionStatus: + mode_prompts_provided: bool = False + project_activation_message_provided: bool = False + + def __init__(self, newly_activated_mode_names: set[str] | None = None): + """ + :param newly_activated_mode_names: list of mode names that have been newly activated (by dynamic project activation) + and for which prompts must still be provided (either in the system prompt or via the activation message) + """ + if newly_activated_mode_names is None: + newly_activated_mode_names = set() + self._newly_activated_mode_names: set[str] = newly_activated_mode_names + self._session_status_dict: dict[str, ProjectPromptProvisionStatus.SessionStatus] = defaultdict(lambda: self.SessionStatus()) + + def _get_session_status(self, session_id: str) -> SessionStatus: + return self._session_status_dict[session_id] + + def is_mode_prompt_already_provided(self, mode_name: str, session_id: str) -> bool: + """ + :param mode_name: the mode name + :param session_id: the client session ID + :return: whether the mode name was already provided (in a project-specific activation message) and therefore + should not be included again (in the Serena instructions manual) + """ + if not self._get_session_status(session_id).mode_prompts_provided: + return False + return mode_name in self._newly_activated_mode_names + + def get_modes_with_prompts_to_be_provided_for_project_activation(self, session_id: str) -> list[SerenaAgentMode]: + """ + Gets the modes that have been newly activated and for which prompts still need to be provided + (in dynamic project activation message). + + :param: session_id: the client session ID + :return: the modes + """ + result = [] + + # Note: We always want to provide the prompts of newly activated modes in the activation message + # because some clients (e.g. Claude Desktop) use a single session for all chats. + # Therefore, we view project activation as an "entry action", which must always provide + # all the information that is relevant to the project + # Because of this, we cannot use a condition like this: + # new_mode_prompts_must_be_provided_for_activation = not self._get_session_status(session_id).mode_prompts_provided + new_mode_prompts_must_be_provided_for_activation = True + mark_used(session_id) + + if new_mode_prompts_must_be_provided_for_activation: + for mode_name in self._newly_activated_mode_names: + mode = ActiveModes.get_mode_instance(mode_name) + if mode.has_prompt(): + result.append(mode) + return result + + def mark_mode_prompts_as_provided(self, session_id: str) -> None: + """ + Marks the prompts for all newly activated modes as provided, so that they will not be included in the project activation message. + + :param session_id: the client session ID + """ + self._get_session_status(session_id).mode_prompts_provided = True + + def mark_project_activation_message_as_provided(self, session_id: str) -> None: + """ + Marks the project activation message as provided, so that it will not be included again in case of multiple activations of the same project. + + :param session_id: the client session ID + """ + self._get_session_status(session_id).project_activation_message_provided = True + + def is_project_activation_message_already_provided(self, session_id: str) -> bool: + """ + :param session_id: the client session ID + :return: whether the project activation message was already provided and therefore should not be included again + """ + return self._get_session_status(session_id).project_activation_message_provided class DashboardManager: @@ -444,7 +520,8 @@ class SerenaAgent: self._project_activation_callback = project_activation_callback self._gui_log_viewer: Optional["GuiLogViewer"] = None self._dashboard_manager: DashboardManager | None = None - + self._project_prompt_status = ProjectPromptProvisionStatus() + self._mode_overrides = modes self.version = serena_version() # obtain serena configuration using the decoupled factory function @@ -453,6 +530,10 @@ class SerenaAgent: # propagate configuration to other components self.serena_config.propagate_settings() + # initialise active modes (baseline modes prior to project activation) + self._active_modes: ActiveModes + self._update_active_modes(log_message=False) + # determine registered project to be activated (if any) registered_project_to_activate: RegisteredProject | None = ( self.serena_config.get_registered_project(project, autoregister=True) if project is not None else None @@ -539,8 +620,6 @@ class SerenaAgent: # activate the given project (if any), also updating the active modes # Note: We cannot update the active tools yet, because the base toolset has not been computed yet # (and its computation depends on the active project) - self._active_modes: ActiveModes - self._mode_overrides = modes if project is not None: try: self.activate_project_from_path_or_name(project, update_active_modes=False, update_active_tools=False) @@ -588,6 +667,7 @@ class SerenaAgent: "dashboard": int(self.serena_config.web_dashboard), "version": self.version, "backend": self._language_backend.value, + "context": self._context.name, } try: requests.get("https://oraios-software.de/serena_usage.php", params=params, timeout=1) @@ -811,7 +891,11 @@ class SerenaAgent: template = JinjaTemplate(prompt_template) return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names) - def create_system_prompt(self) -> str: + def create_system_prompt(self, session_id: str = "global") -> str: + """ + :param session_id: the client session ID for the case where this is run from a tool; "global" for the connection time case + :return: the prompt + """ available_tools = self._active_tools available_markers = available_tools.tool_marker_names global_memories = MemoriesManager( @@ -819,28 +903,48 @@ class SerenaAgent: ).list_global_memories() global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else "" log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers) + + # determine modes for which prompts must (still) be provided, excluding modes that were already provided in a + # previously provided project activation message (if any) + relevant_modes = [] + for mode in self.get_active_modes(): + if mode.has_prompt(): + if not self._project_prompt_status.is_mode_prompt_already_provided(mode.name, session_id): + relevant_modes.append(mode) + self._project_prompt_status.mark_mode_prompts_as_provided(session_id) + system_prompt = self.prompt_factory.create_system_prompt( context_system_prompt=self._format_prompt(self._context.prompt), - mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self.get_active_modes()], + mode_system_prompts=[self._format_prompt(mode.prompt) for mode in relevant_modes], available_tools=available_tools.tool_names, available_markers=available_markers, global_memories_list=global_memories_str, ) - # If a project is active at startup, append its activation message - if self._active_project is not None: - system_prompt += "\n\n" + self.get_project_activation_message() + # provide the project activation message if it hasn't yet been provided + if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id): + system_prompt += "\n\n" + self.get_project_activation_message(session_id) log.info("System prompt:\n%s", system_prompt) return system_prompt - def get_project_activation_message(self) -> str: + def get_project_activation_message(self, session_id: str) -> str: """ :return: a message providing information about the project upon activation (e.g. programming language, memories, initial prompt) :raise: AssertionError if no project is active """ proj = self._active_project assert proj is not None, "A project must be active before calling this." + + # Note: The activation message is always returned in full, even if it was already provided in the current session, + # because some clients (e.g. Claude Desktop) will use the same session across multiple chats. + # So while we don't want the activation message to be additionally included in the system prompt + # (initial_instructions), an explicit project activation should always return it. + # (The check below deliberately left in place for documentation purposes, preventing a regression) + if self._project_prompt_status.is_project_activation_message_already_provided(session_id): + pass # no special handling + + # provide basic project information (name, location, languages, encoding) if proj.is_newly_created: msg = f"Created and activated a new project with name '{proj.project_name}' at {proj.project_root}. " else: @@ -850,6 +954,7 @@ class SerenaAgent: msg += f"\nProgramming languages: {languages_str}." msg += f"File encoding: {proj.project_config.encoding}." + # add list of memories (if memories are enabled) include_memories = self._active_tools.contains_tool_class(ReadMemoryTool) if include_memories: project_memories = proj.memories_manager.list_project_memories() @@ -858,11 +963,24 @@ class SerenaAgent: f"\n{json.dumps(project_memories.to_dict())}\n" + "Use the `read_memory` tool to read these memories later if they are relevant to the task." ) + + # add prompts for modes that were dynamically activated by the project + modes_with_prompts = self._project_prompt_status.get_modes_with_prompts_to_be_provided_for_project_activation(session_id) + if modes_with_prompts: + msg += "\nNewly applicable mode instructions:" + for mode in modes_with_prompts: + msg += f"\n{mode.prompt}" + self._project_prompt_status.mark_mode_prompts_as_provided(session_id) + + # add project-specific prompt if proj.project_config.initial_prompt: - msg += f"\nAdditional project-specific instructions:\n {proj.project_config.initial_prompt}" + msg += f"\nProject-specific instructions:\n {proj.project_config.initial_prompt}" + + self._project_prompt_status.mark_project_activation_message_as_provided(session_id) + return msg - def _update_active_modes(self) -> None: + def _update_active_modes(self, log_message: bool = True) -> None: """ Updates the active modes based on the Serena configuration, the active project configuration (if any), and mode overrides (if any). @@ -873,6 +991,9 @@ class SerenaAgent: self._active_modes.apply(self._active_project.project_config) if self._mode_overrides: self._active_modes.apply(self._mode_overrides) + if log_message: + active_mode_names = self._active_modes.get_mode_names() + log.info(f"Active modes ({len(active_mode_names)}): {', '.join(active_mode_names)}") def _update_active_tools(self) -> None: """ @@ -964,7 +1085,13 @@ class SerenaAgent: project.set_agent(self) if update_active_modes: + active_mode_names_before = set(self._active_modes.get_mode_names()) self._update_active_modes() + newly_activated_mode_names = set(self._active_modes.get_mode_names()) - active_mode_names_before + else: + newly_activated_mode_names = None + + self._project_prompt_status = ProjectPromptProvisionStatus(newly_activated_mode_names=newly_activated_mode_names) if update_active_tools: self._update_active_tools() diff --git a/src/serena/cli.py b/src/serena/cli.py index 8d31ecd1..5e87fa2e 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -396,7 +396,6 @@ class TopLevelCommands(AutoRegisteringGroup): ) -> None: prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account." postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks." - from serena.tools.workflow_tools import InitialInstructionsTool lvl = logging.getLevelNamesMapping()[log_level.upper()] logging.configure(level=lvl) @@ -410,8 +409,7 @@ class TopLevelCommands(AutoRegisteringGroup): context=context_instance, modes=modes_selection_def, ) - tool = agent.get_tool(InitialInstructionsTool) - instr = tool.apply() + instr = agent.create_system_prompt() if only_instructions: print(instr) else: diff --git a/src/serena/code_editor.py b/src/serena/code_editor.py index 2232f66b..2392d2ba 100644 --- a/src/serena/code_editor.py +++ b/src/serena/code_editor.py @@ -135,6 +135,13 @@ class CodeEditor(Generic[TSymbol], ABC): Inserts content after the symbol with the given name in the given file. """ symbol = self._find_unique_symbol(name_path, relative_file_path) + # Note: for body to be available, the symbol dto that the symbol instance is built from + # must have been retrieved either with body or at least with location. + # since _find_unique_symbol passes include_location=True, it works here + if symbol.body == symbol.name: + raise ValueError( + f"Cannot insert after this symbol (not a function, class or method): {symbol}. Consider using insert_before_symbol instead." + ) # make sure body always ends with at least one newline if not body.endswith("\n"): diff --git a/src/serena/config/context_mode.py b/src/serena/config/context_mode.py index 053f54ee..bb3164c9 100644 --- a/src/serena/config/context_mode.py +++ b/src/serena/config/context_mode.py @@ -136,6 +136,12 @@ class SerenaAgentMode(ToolInclusionDefinition, ToStringMixin): return cls.from_name(str(name_or_path)) + def has_prompt(self) -> bool: + """ + :return: whether this mode defines a prompt + """ + return bool(self.prompt and self.prompt.strip()) + @dataclass(kw_only=True) class SerenaAgentContext(ToolInclusionDefinition, ToStringMixin): diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 2f787f6d..5c8e4e10 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -418,6 +418,10 @@ class ProjectConfig(SharedConfig): data["languages"] = [data["language"]] del data["language"] + # Note: Checks for validity of fields must not happen here but in _from_dict. + # Here, the data may be incomplete, because this function is also used for + # loading project.local.yml files. + return data, was_complete @classmethod @@ -461,13 +465,19 @@ class ProjectConfig(SharedConfig): line_ending_value = data.get("line_ending") line_ending = LineEnding.from_str(line_ending_value) if line_ending_value else None + # gracefully handle user errors: incorrect use of None/empty where a list is required + ignored_paths = data["ignored_paths"] or [] + fixed_tools = data["fixed_tools"] or [] + excluded_tools = data["excluded_tools"] or [] + included_optional_tools = data["included_optional_tools"] or [] + return cls( project_name=data["project_name"], languages=languages, - ignored_paths=data["ignored_paths"], - excluded_tools=data["excluded_tools"], - fixed_tools=data["fixed_tools"], - included_optional_tools=data["included_optional_tools"], + ignored_paths=ignored_paths, + excluded_tools=excluded_tools, + fixed_tools=fixed_tools, + included_optional_tools=included_optional_tools, read_only=data["read_only"], read_only_memory_patterns=data.get("read_only_memory_patterns", []), ignored_memory_patterns=data.get("ignored_memory_patterns", []), diff --git a/src/serena/project.py b/src/serena/project.py index 6f1cec43..a7b15f53 100644 --- a/src/serena/project.py +++ b/src/serena/project.py @@ -79,7 +79,10 @@ class MemoriesManager: def get_memory_file_path(self, name: str) -> Path: # Strip .md extension if present - name = name.replace(".md", "") + name = name.replace(".md", "").replace(os.sep, "/") + parts = name.split("/") + if ".." in parts: + raise ValueError(f"Memory name cannot contain '..' segments for security reasons. Got: {name}") if self._is_global(name): if name == self.GLOBAL_TOPIC: @@ -98,7 +101,7 @@ class MemoriesManager: # Project-local memory assert self._project_memory_dir is not None, "Project dir was not passed at initialization" - parts = name.split("/") + filename = f"{parts[-1]}.md" if len(parts) > 1: @@ -303,39 +306,42 @@ class Project(ToStringMixin): f.write(f"/{ProjectConfig.SERENA_LOCAL_PROJECT_FILE}\n") # prepare ignore spec asynchronously, ensuring immediate project activation. - self.__ignored_patterns: list[str] - self.__ignore_spec: pathspec.PathSpec + self.__ignored_patterns: list[str] | None = None + self.__ignore_spec: pathspec.PathSpec | None = None self._ignore_spec_available = threading.Event() threading.Thread(name=f"gather-ignorespec[{self.project_config.project_name}]", target=self._gather_ignorespec, daemon=True).start() def _gather_ignorespec(self) -> None: with LogTime(f"Gathering ignore spec for project {self.project_config.project_name}", logger=log): - # gather ignored paths from the global configuration, project configuration, and gitignore files - global_ignored_paths = self.serena_config.ignored_paths - ignored_patterns = list(global_ignored_paths) + list(self.project_config.ignored_paths) - if len(global_ignored_paths) > 0: - log.info(f"Using {len(global_ignored_paths)} ignored paths from the global configuration.") - log.debug(f"Global ignored paths: {list(global_ignored_paths)}") - if len(self.project_config.ignored_paths) > 0: - log.info(f"Using {len(self.project_config.ignored_paths)} ignored paths from the project configuration.") - log.debug(f"Project ignored paths: {self.project_config.ignored_paths}") - log.debug(f"Combined ignored patterns: {ignored_patterns}") - if self.project_config.ignore_all_files_in_gitignore: - gitignore_parser = GitignoreParser(self.project_root) - for spec in gitignore_parser.get_ignore_specs(): - log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") - ignored_patterns.extend(spec.patterns) - self.__ignored_patterns = ignored_patterns + try: + # gather ignored paths from the global configuration, project configuration, and gitignore files + global_ignored_paths = self.serena_config.ignored_paths + ignored_patterns = list(global_ignored_paths) + list(self.project_config.ignored_paths) + if len(global_ignored_paths) > 0: + log.info(f"Using {len(global_ignored_paths)} ignored paths from the global configuration.") + log.debug(f"Global ignored paths: {list(global_ignored_paths)}") + if len(self.project_config.ignored_paths) > 0: + log.info(f"Using {len(self.project_config.ignored_paths)} ignored paths from the project configuration.") + log.debug(f"Project ignored paths: {self.project_config.ignored_paths}") + log.debug(f"Combined ignored patterns: {ignored_patterns}") + if self.project_config.ignore_all_files_in_gitignore: + gitignore_parser = GitignoreParser(self.project_root) + for spec in gitignore_parser.get_ignore_specs(): + log.debug(f"Adding {len(spec.patterns)} patterns from {spec.file_path} to the ignored paths.") + ignored_patterns.extend(spec.patterns) + self.__ignored_patterns = ignored_patterns - # Set up the pathspec matcher for the ignored paths - # for all absolute paths in ignored_paths, convert them to relative paths - processed_patterns = [] - for pattern in ignored_patterns: - # Normalize separators (pathspec expects forward slashes) - pattern = pattern.replace(os.path.sep, "/") - processed_patterns.append(pattern) - log.debug(f"Processing {len(processed_patterns)} ignored paths") - self.__ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns) + # Set up the pathspec matcher for the ignored paths + # for all absolute paths in ignored_paths, convert them to relative paths + processed_patterns = [] + for pattern in ignored_patterns: + # Normalize separators (pathspec expects forward slashes) + pattern = pattern.replace(os.path.sep, "/") + processed_patterns.append(pattern) + log.debug(f"Processing {len(processed_patterns)} ignored paths") + self.__ignore_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, processed_patterns) + except Exception as e: + log.error(f"Error while gathering ignore spec for project {self.project_config.project_name}: {e}", exc_info=e) self._ignore_spec_available.set() @@ -397,7 +403,12 @@ class Project(ToStringMixin): if not self._ignore_spec_available.is_set(): log.info("Waiting for ignore spec to become available ...") self._ignore_spec_available.wait() - log.info("Ignore spec is now available for project; proceeding") + if self.__ignore_spec is not None: + log.info("Ignore spec is now available for project; proceeding") + if self.__ignore_spec is None: + raise ValueError( + "The ignore spec could not be computed; please check the log for errors and report here: https://github.com/oraios/serena/issues" + ) return self.__ignore_spec @property @@ -408,7 +419,12 @@ class Project(ToStringMixin): if not self._ignore_spec_available.is_set(): log.info("Waiting for ignored patterns to become available ...") self._ignore_spec_available.wait() - log.info("Ignore patterns are now available for project; proceeding") + if self.__ignored_patterns is not None: + log.info("Ignored patterns are now available for project; proceeding") + if self.__ignored_patterns is None: + raise ValueError( + "The ignored patterns could not be computed; please check the log for errors and report here: https://github.com/oraios/serena/issues" + ) return self.__ignored_patterns def _is_ignored_relative_path(self, relative_path: str | Path, ignore_non_source_files: bool = True) -> bool: diff --git a/src/serena/project_server.py b/src/serena/project_server.py index dfe5c291..bd6c7a79 100644 --- a/src/serena/project_server.py +++ b/src/serena/project_server.py @@ -53,7 +53,7 @@ class ProjectServer: serena_config.language_backend = LanguageBackend.LSP self._agent = SerenaAgent(serena_config=serena_config) - self._loaded_projects_by_name: dict[str, "Project"] = {} + self._loaded_projects_by_root: dict[str, "Project"] = {} # create the Flask application self._app = Flask(__name__) @@ -69,26 +69,31 @@ class ProjectServer: query_request = QueryProjectRequest.model_validate(request.get_json()) return self._query_project(query_request) - def _get_project(self, project_name: str) -> "Project": + def _get_project(self, project_root_or_name: str) -> "Project": """Gets the project with the given name, loading it if necessary.""" - if project_name in self._loaded_projects_by_name: - return self._loaded_projects_by_name[project_name] - else: - serena_config = self._agent.serena_config - registered_project = serena_config.get_registered_project(project_name) - if registered_project is None: - raise ValueError(f"Project '{project_name}' is not registered with Serena.") - with LogTime(f"Loading project '{project_name}'"): - project = registered_project.get_project_instance(serena_config) - project.create_language_server_manager() - self._loaded_projects_by_name[project_name] = project - return project + serena_config = self._agent.serena_config + registered_project = serena_config.get_registered_project(project_root_or_name) + if registered_project is None: + raise ValueError(f"Project '{project_root_or_name}' is not registered with Serena.") + + key = str(registered_project.project_root) + + if key in self._loaded_projects_by_root: + return self._loaded_projects_by_root[key] + + with LogTime(f"Loading project '{project_root_or_name}'"): + project = registered_project.get_project_instance(serena_config) + project.create_language_server_manager() + self._loaded_projects_by_root[key] = project + return project def _query_project(self, req: QueryProjectRequest) -> str: """Handle a /query_project request by invoking the agent on the specified project and tool.""" project = self._get_project(req.project_name) with self._agent.active_project_context(project): tool = self._agent.get_tool_by_name(req.tool_name) + if not tool.is_readonly(): + raise ValueError(f"Tool '{req.tool_name}' is not read-only and cannot be executed via the query_project route") params = json.loads(req.tool_params_json) return tool.apply_ex(**params) diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml index b6d632a7..fe63878b 100644 --- a/src/serena/resources/config/contexts/claude-code.yml +++ b/src/serena/resources/config/contexts/claude-code.yml @@ -7,16 +7,15 @@ prompt: | **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them right now. For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools. - For edits, where it makes sense, use Serena's editing tools since they are more token efficient. - Use your own editing tools only if Serena's editing tools would be suboptimal. - **IMPORTANT**: Never use a tool before reading its schema via the tool-search tool! + For edits, prefer Serena's editing tools since they are more token efficient. + Especially when editing code you have read through Serena, ALWAYS use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content). + **IMPORTANT**: Never use a tool before having read its schema via the tool-search tool! excluded_tools: - create_text_file - read_file - execute_shell_command - - replace_content - find_file - list_dir - search_for_pattern diff --git a/src/serena/resources/config/contexts/junie.yml b/src/serena/resources/config/contexts/junie.yml index e8806f4e..8bfdc67d 100644 --- a/src/serena/resources/config/contexts/junie.yml +++ b/src/serena/resources/config/contexts/junie.yml @@ -12,6 +12,10 @@ excluded_tools: - create_text_file - read_file - execute_shell_command + - replace_content + - find_file + - list_dir + - search_for_pattern tool_description_overrides: {} diff --git a/src/serena/resources/dashboard/dashboard.js b/src/serena/resources/dashboard/dashboard.js index df53868e..e48ff3c2 100644 --- a/src/serena/resources/dashboard/dashboard.js +++ b/src/serena/resources/dashboard/dashboard.js @@ -485,32 +485,9 @@ class Dashboard { // Initialize the application this.loadToolNames().then(function () { - // Start on overview page self.loadNews(); - self.loadConfigOverview(); - self.startConfigPolling(); - self.startExecutionsPolling(); - }); - // Initialize heartbeat interval - setInterval(this.heartbeat.bind(this), 250); - } - - heartbeat() { - let self = this; - $.ajax({ - url: '/heartbeat', - type: 'GET', - success: function (response) { - self.heartbeatFailureCount = 0; - }, - error: function (xhr, status, error) { - self.heartbeatFailureCount++; - console.error('Heartbeat failure; count = ', self.heartbeatFailureCount); - if (self.heartbeatFailureCount >= 1) { - console.log('Server appears to be down, closing tab'); - window.close(); - } - }, + // start on overview page + self.navigateToPage("overview"); }); } @@ -541,7 +518,6 @@ class Dashboard { // Start appropriate polling for the page if (page === 'overview') { this.loadNews(); - this.loadConfigOverview(); this.startConfigPolling(); this.startExecutionsPolling(); } else if (page === 'logs') { @@ -599,7 +575,8 @@ class Dashboard { } else { console.log('Config unchanged, skipping display update'); } - }, error: function (xhr, status, error) { + }, + error: function (xhr, status, error) { console.error('Error loading config overview:', error); self.$configDisplay.html('
Error loading configuration
'); self.$basicStatsDisplay.html('
Error loading stats
'); @@ -607,24 +584,23 @@ class Dashboard { self.$availableToolsDisplay.html('
Error loading tools
'); self.$availableModesDisplay.html('
Error loading modes
'); self.$availableContextsDisplay.html('
Error loading contexts
'); - }, complete: function () { + }, + complete: function () { self.waitingForConfigPollingResult = false; } }); } startConfigPolling() { + this.loadConfigOverview(); this.configPollInterval = setInterval(this.loadConfigOverview.bind(this), 1000); } startExecutionsPolling() { + this.loadExecutions() // Poll every 1 second for executions (independent of config polling) // This ensures stuck executions can still be cancelled even if config polling is blocked - this.loadExecutions() - this.executionsPollInterval = setInterval(() => { - this.loadQueuedExecutions(); - this.loadLastExecution(); - }, 1000); + this.executionsPollInterval = setInterval(this.loadExecutions.bind(this), 1000); } displayConfig(config) { @@ -905,26 +881,30 @@ class Dashboard { // ===== Executions Methods ===== - loadQueuedExecutions() { + loadQueuedExecutions(onComplete) { let self = this; $.ajax({ - url: '/queued_task_executions', type: 'GET', success: function (response) { + url: '/queued_task_executions', type: 'GET', + success: function (response) { if (response.status === 'success') { self.displayActiveExecutionsQueue(response.queued_executions || []); } else { console.error('Error loading executions:', response.message); } - }, error: function (xhr, status, error) { + }, + error: function (xhr, status, error) { console.error('Error loading executions:', error); self.$activeExecutionQueueDisplay.html('
Error loading executions
'); - } + }, + complete: onComplete }); } - loadLastExecution() { + loadLastExecution(onComplete) { let self = this; $.ajax({ - url: '/last_execution', type: 'GET', success: function (response) { + url: '/last_execution', type: 'GET', + success: function (response) { if (response.status === 'success') { if (response.last_execution !== null && response.last_execution.logged) { self.displayLastExecution(response.last_execution); @@ -932,21 +912,28 @@ class Dashboard { } else { console.error('Error loading last execution:', response.message); } - }, error: function (xhr, status, error) { + }, + error: function (xhr, status, error) { console.error('Error loading last execution:', error); self.$lastExecutionDisplay.html('
Error loading last execution
'); - } + }, + complete: onComplete }); } loadExecutions() { + const self = this; if (this.waitingForExecutionsPollingResult) { console.log('Still waiting for previous executions poll result, skipping this poll'); - } else { + } + else { this.waitingForExecutionsPollingResult = true; console.log('Polling for executions...'); - this.loadQueuedExecutions(); - this.loadLastExecution(); + this.loadQueuedExecutions(function() { + self.loadLastExecution(function() { + self.waitingForExecutionsPollingResult = false; + }); + }); } } diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 72aa4bbd..4a1b14cc 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -15,6 +15,7 @@ from solidlsp import SolidLanguageServer from solidlsp.ls import LSPFileBuffer from solidlsp.ls import ReferenceInSymbol as LSPReferenceInSymbol from solidlsp.ls_types import Position, SymbolKind, UnifiedSymbolInformation +from solidlsp.ls_utils import TextUtils from .ls_manager import LanguageServerManager from .project import Project @@ -89,6 +90,16 @@ class Symbol(ToStringMixin, ABC): def get_body_end_position(self) -> PositionInFile | None: pass + @property + @abstractmethod + def body(self) -> str | None: + pass + + @property + @abstractmethod + def name(self) -> str: + pass + def get_body_start_position_or_raise(self) -> PositionInFile: """ Get the start position of the symbol body, raising an error if it is not defined. @@ -879,6 +890,7 @@ class JetBrainsSymbol(Symbol): self._cached_file_content: str | None = None self._cached_body_start_position: PositionInFile | None = None self._cached_body_end_position: PositionInFile | None = None + self._cached_body = symbol_dict.get("body") def _tostring_includes(self) -> list[str]: return [] @@ -920,6 +932,25 @@ class JetBrainsSymbol(Symbol): self._cached_body_end_position = PositionInFile(line=line, col=col) return self._cached_body_end_position + @property + def body(self) -> str | None: + if self._cached_body is not None: + return self._cached_body + start_position = self.get_body_start_position() + if start_position is None: + return None + end_position = self.get_body_end_position() + assert end_position is not None, "If start position is available, end position should also be available. Symbol: {self}" + file_content = self.get_file_content() + self._cached_body = TextUtils.get_text_in_range( + file_content, start_position.line, start_position.col, end_position.line, end_position.col + ) + return self._cached_body + + @property + def name(self) -> str: + return self._dict["name_path"].split("/")[-1] + def is_neighbouring_definition_separated_by_empty_line(self) -> bool: # NOTE: Symbol types cannot really be differentiated, because types are not handled in a language-agnostic way. return False diff --git a/src/serena/tools/config_tools.py b/src/serena/tools/config_tools.py index 2008012b..12e4baf0 100644 --- a/src/serena/tools/config_tools.py +++ b/src/serena/tools/config_tools.py @@ -1,3 +1,5 @@ +from sensai.util.helper import mark_used + from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional @@ -22,17 +24,17 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject): Activates a project based on the project name or path. """ - def apply(self, project: str) -> str: + # noinspection PyIncorrectDocstring + # (session_id is injected via apply_ex) + def apply(self, project: str, session_id: str) -> str: """ Activates the project with the given name or path. :param project: the name of a registered project to activate or a path to a project directory """ is_new_activation = self.agent.activate_project_from_path_or_name(project) - if not is_new_activation: - result = "Project was already active." - else: - result = self.agent.get_project_activation_message() + mark_used(is_new_activation) + result = self.agent.get_project_activation_message(session_id) result += "\nIMPORTANT: If you have not yet read the 'Serena Instructions Manual', do it now before continuing!" return result diff --git a/src/serena/tools/query_project_tools.py b/src/serena/tools/query_project_tools.py index b675a0aa..2820ab29 100644 --- a/src/serena/tools/query_project_tools.py +++ b/src/serena/tools/query_project_tools.py @@ -16,6 +16,7 @@ class ListQueryableProjectsTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequi Lists available projects that can be queried with `query_project_tool`. :param symbol_access: whether to return only projects for which symbol access is available. Default: true + :return: project names and roots """ # determine relevant projects registered_projects = self.agent.serena_config.projects @@ -31,12 +32,9 @@ class ListQueryableProjectsTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequi else: relevant_projects = registered_projects - # return project names, excluding the active project (if any) - project_names = [p.project_name for p in relevant_projects] - active_project = self.agent.get_active_project() - if active_project is not None: - project_names = [n for n in project_names if n != active_project.project_name] - return self._to_json(project_names) + # return project names and roots + result = {p.project_name: str(p.project_root) for p in relevant_projects} + return self._to_json(result) class QueryProjectTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject): @@ -50,7 +48,7 @@ class QueryProjectTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveP Queries a project by executing a read-only Serena tool. The tool will be executed in the context of the project. Use this to query information from projects other than the activated project. - :param project_name: the name of the project to query + :param project_name: the name of the project to query (or root path) :param tool_name: the name of the tool to execute in the other project. The tool must be read-only. :param tool_params_json: the parameters to pass to the tool, encoded as a JSON string """ diff --git a/src/serena/tools/symbol_tools.py b/src/serena/tools/symbol_tools.py index 40340e40..a19a99b9 100644 --- a/src/serena/tools/symbol_tools.py +++ b/src/serena/tools/symbol_tools.py @@ -369,8 +369,8 @@ class InsertAfterSymbolTool(Tool, ToolMarkerSymbolicEdit): body: str, ) -> str: """ - Inserts the given body/content after the end of the definition of the given symbol (via the symbol's location). - A typical use case is to insert a new class, function, method, field or variable assignment. + Use this to insert code after a class/method/function definition. + Don't use to insert after assignments (constants, fields). :param name_path: name path of the symbol after which to insert content (definitions in the `find_symbol` tool apply) :param relative_path: the relative path to the file containing the symbol diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 7a453ae0..c13e4cd4 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -3,6 +3,7 @@ import json from abc import ABC from collections.abc import Callable, Iterable from dataclasses import dataclass +from functools import cached_property from types import TracebackType from typing import TYPE_CHECKING, Any, Protocol, Self, TypeVar, cast @@ -134,9 +135,32 @@ class Tool(Component): # (which is use by the LLM, so a good description is important) # and to validate the tool call arguments. + SESSION_ID_PARAM_NAME = "session_id" + """ + parameter name to use in apply method for the client session ID. + This parameter will be ignored by the MCP interface but will be populated with the session ID of the current client session + when the tool is called, allowing tools to be session-aware if needed. + """ + _last_tool_call_client_str: str | None = None """We can only get the client info from within a tool call. Each tool call will update this variable.""" + def __init__(self, agent: "SerenaAgent"): + super().__init__(agent) + + @cached_property + def _is_session_aware(self) -> bool: + """ + :return: whether the tool is session-aware, i.e. whether the apply method expects a session_id (str) parameter. + """ + # check apply method for session_id arg + apply_fn = self.get_apply_fn() + sig = inspect.signature(apply_fn) + for param in sig.parameters.values(): + if param.name == self.SESSION_ID_PARAM_NAME: + return True + return False + @staticmethod def _sanitize_input_param(raw_param: str) -> str: # some clients replace < and > with their escaped html versions, we need to counteract this @@ -225,9 +249,9 @@ class Tool(Component): if apply_fn is None: raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") - return func_metadata(apply_fn, skip_names=["self", "cls"]) + return func_metadata(apply_fn, skip_names=["self", "cls", cls.SESSION_ID_PARAM_NAME]) - def _log_tool_application(self, frame: Any) -> None: + def _log_tool_application(self, frame: Any, session_id: str) -> None: params = {} ignored_params = {"self", "log_call", "catch_exceptions", "args", "apply_fn"} for param, value in frame.f_locals.items(): @@ -237,7 +261,7 @@ class Tool(Component): params.update(value) else: params[param] = value - log.info(f"{self.get_name_from_cls()}: {dict_string(params)}") + log.info(f"{self.get_name_from_cls()}: {dict_string(params)}; session_id: {session_id}") def _limit_length( self, @@ -284,8 +308,10 @@ class Tool(Component): """ Applies the tool with logging and exception handling, using the given keyword arguments """ + session_id = "global" if mcp_ctx is not None: try: + session_id = "%x" % id(mcp_ctx.session) client_params = mcp_ctx.session.client_params if client_params is not None: client_info = cast(Implementation, client_params.clientInfo) @@ -306,7 +332,7 @@ class Tool(Component): return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" if log_call: - self._log_tool_application(inspect.currentframe()) + self._log_tool_application(inspect.currentframe(), session_id) try: # check whether the tool requires an active project and language server if not isinstance(self, ToolMarkerDoesNotRequireActiveProject): @@ -316,9 +342,14 @@ class Tool(Component): + f"{self.agent.serena_config.project_names}" ) + # construct apply kwargs, adding session_id if the tool is session-aware + apply_kwargs = dict(kwargs) + if self._is_session_aware: + apply_kwargs["session_id"] = session_id + # apply the actual tool try: - result = apply_fn(**kwargs) + result = apply_fn(**apply_kwargs) except SolidLSPException as e: if e.is_language_server_terminated(): affected_language = e.get_affected_language() @@ -327,7 +358,7 @@ class Tool(Component): f"Language server terminated while executing tool ({e}). Restarting the language server and retrying ..." ) self.agent.get_language_server_manager_or_raise().restart_language_server(affected_language) - result = apply_fn(**kwargs) + result = apply_fn(**apply_kwargs) else: log.error( f"Language server terminated while executing tool ({e}), but affected language is unknown. Not retrying." @@ -337,7 +368,7 @@ class Tool(Component): raise # record tool usage - self.agent.record_tool_usage(kwargs, result, self) + self.agent.record_tool_usage(apply_kwargs, result, self) except Exception as e: if not catch_exceptions: diff --git a/src/serena/tools/workflow_tools.py b/src/serena/tools/workflow_tools.py index 7b1f5d96..e50ec9b5 100644 --- a/src/serena/tools/workflow_tools.py +++ b/src/serena/tools/workflow_tools.py @@ -63,10 +63,12 @@ class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject): for clients that do not read the initial instructions when the MCP server is connected. """ - def apply(self) -> str: + # noinspection PyIncorrectDocstring + # (session_id is injected via apply_ex) + def apply(self, session_id: str) -> str: """ Provides the 'Serena Instructions Manual', which contains essential information on how to use the Serena toolbox. IMPORTANT: If you have not yet read the manual, call this tool immediately after you are given your task by the user, as it will critically inform you! """ - return self.agent.create_system_prompt() + return self.agent.create_system_prompt(session_id=session_id) diff --git a/src/solidlsp/ls_utils.py b/src/solidlsp/ls_utils.py index 9db2a9a4..b54654b8 100644 --- a/src/solidlsp/ls_utils.py +++ b/src/solidlsp/ls_utils.py @@ -115,6 +115,15 @@ class TextUtils: new_l, new_c = TextUtils._get_updated_position_from_line_and_column_and_edit(line, col, text_to_be_inserted) return new_text, new_l, new_c + @staticmethod + def get_text_in_range(text: str, start_line: int, start_col: int, end_line: int, end_col: int) -> str: + """ + Returns the text between the given start and end positions. + """ + start_idx = TextUtils.get_index_from_line_col(text, start_line, start_col) + end_idx = TextUtils.get_index_from_line_col(text, end_line, end_col) + return text[start_idx:end_idx] + class PathUtils: """ diff --git a/src/solidlsp/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py index ffb6135e..c96f5fce 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_types.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_types.py @@ -2456,8 +2456,8 @@ class DocumentSymbol(TypedDict): selectionRange: "Range" """ The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. Must be contained by the `range`. """ - - # TODO: I think this type is missing the 'children' field - DJ + children: NotRequired[list["DocumentSymbol"]] + """ Children of this symbol, e.g. properties of a class. """ class DocumentSymbolRegistrationOptions(TypedDict): diff --git a/test/serena/__snapshots__/test_symbol_editing.ambr b/test/serena/__snapshots__/test_symbol_editing.ambr index f614627e..6c688345 100644 --- a/test/serena/__snapshots__/test_symbol_editing.ambr +++ b/test/serena/__snapshots__/test_symbol_editing.ambr @@ -327,7 +327,6 @@ # Module-level variable with type annotation typed_module_var: int = 42 - new_module_var = "Inserted after typed_module_var" # Regular class with class and instance variables @@ -392,6 +391,9 @@ other_result = reassignable_module_var * 2 return result, other_result + def new_inserted_function(): + print("This is a new function inserted before another.") + # Create instances and use variables dataclass_instance = VariableDataclass(id=1, name="Test") @@ -428,7 +430,6 @@ reassignable_module_var = 20 # Reassigned # Module-level variable with type annotation - new_module_var = "Inserted after typed_module_var" typed_module_var: int = 42 @@ -488,6 +489,9 @@ # Function that uses the module variables + def new_inserted_function(): + print("This is a new function inserted before another.") + def use_module_variables(): """Function that uses module-level variables.""" result = module_var + " used in function" @@ -511,209 +515,59 @@ # --- # name: test_insert_in_rel_to_symbol[test_case1-after] ''' - """ - Test module for variable declarations and usage. + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } - from dataclasses import dataclass, field + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } - # Module-level variables - module_var = "Initial module value" + helperFunction(); - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - def new_inserted_function(): - print("This is a new function inserted before another.") - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } ''' # --- # name: test_insert_in_rel_to_symbol[test_case1-before] ''' - """ - Test module for variable declarations and usage. + function newFunctionAfterClass(): void { + console.log("This function is after DemoClass."); + } - This module tests various types of variable declarations and usages including: - - Module-level variables - - Class-level variables - - Instance variables - - Variable reassignments - """ + export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } + } - from dataclasses import dataclass, field + export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); + } - # Module-level variables - module_var = "Initial module value" + helperFunction(); - reassignable_module_var = 10 - reassignable_module_var = 20 # Reassigned - - # Module-level variable with type annotation - typed_module_var: int = 42 - - - # Regular class with class and instance variables - class VariableContainer: - """Class that contains various variables.""" - - # Class-level variables - class_var = "Initial class value" - - reassignable_class_var = True - reassignable_class_var = False # Reassigned #noqa: PIE794 - - # Class-level variable with type annotation - typed_class_var: str = "typed value" - - def __init__(self): - # Instance variables - self.instance_var = "Initial instance value" - self.reassignable_instance_var = 100 - - # Instance variable with type annotation - self.typed_instance_var: list[str] = ["item1", "item2"] - - def modify_instance_var(self): - # Reassign instance variable - self.instance_var = "Modified instance value" - self.reassignable_instance_var = 200 # Reassigned - - def use_module_var(self): - # Use module-level variables - result = module_var + " used in method" - other_result = reassignable_module_var + 5 - return result, other_result - - def use_class_var(self): - # Use class-level variables - result = VariableContainer.class_var + " used in method" - other_result = VariableContainer.reassignable_class_var - return result, other_result - - - # Dataclass with variables - @dataclass - class VariableDataclass: - """Dataclass that contains various fields.""" - - # Field variables with type annotations - id: int - name: str - items: list[str] = field(default_factory=list) - metadata: dict[str, str] = field(default_factory=dict) - optional_value: float | None = None - - # This will be reassigned in various places - status: str = "pending" - - - # Function that uses the module variables - def new_inserted_function(): - print("This is a new function inserted before another.") - - def use_module_variables(): - """Function that uses module-level variables.""" - result = module_var + " used in function" - other_result = reassignable_module_var * 2 - return result, other_result - - - # Create instances and use variables - dataclass_instance = VariableDataclass(id=1, name="Test") - dataclass_instance.status = "active" # Reassign dataclass field - - # Use variables at module level - module_result = module_var + " used at module level" - other_module_result = reassignable_module_var + 30 - - # Create a second dataclass instance with different status - second_dataclass = VariableDataclass(id=2, name="Another Test") - second_dataclass.status = "completed" # Another reassignment of status + export function unusedStandaloneFunction(): string { + return "This function is not referenced anywhere"; + } ''' # --- @@ -729,64 +583,6 @@ } } - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - export function unusedStandaloneFunction(): string { - return "This function is not referenced anywhere"; - } - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case2-before] - ''' - function newFunctionAfterClass(): void { - console.log("This function is after DemoClass."); - } - - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - - export function helperFunction() { - const demo = new DemoClass(42); - demo.printValue(); - } - - helperFunction(); - - export function unusedStandaloneFunction(): string { - return "This function is not referenced anywhere"; - } - - ''' -# --- -# name: test_insert_in_rel_to_symbol[test_case3-after] - ''' - export class DemoClass { - value: number; - constructor(value: number) { - this.value = value; - } - printValue() { - console.log(this.value); - } - } - export function helperFunction() { const demo = new DemoClass(42); demo.printValue(); @@ -804,7 +600,7 @@ ''' # --- -# name: test_insert_in_rel_to_symbol[test_case3-before] +# name: test_insert_in_rel_to_symbol[test_case2-before] ''' export class DemoClass { value: number; diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index be078eae..896b030f 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -14,11 +14,14 @@ from serena.config.serena_config import ProjectConfig, RegisteredProject, Serena from serena.project import Project from serena.tools import ( SUCCESS_RESULT, + ActivateProjectTool, FindReferencingSymbolsTool, FindSymbolTool, + InitialInstructionsTool, ReplaceContentTool, ReplaceSymbolBodyTool, SafeDeleteSymbol, + Tool, ) from solidlsp.ls_config import Language from solidlsp.ls_types import SymbolKind @@ -697,3 +700,96 @@ class TestSerenaAgent: assert name_path not in file_content, ( f"Expected symbol {name_path} to be removed from {relative_path}, but it still appears in the file content" ) + + +class TestPromptProvision: + class MockContext: + def __init__(self, session_id: str): + self.session = session_id + + @classmethod + def _call_tool(cls, agent: SerenaAgent, tool_class: type[Tool], session_id: str = "global", **kwargs) -> str: + result = agent.get_tool(tool_class).apply_ex(mcp_ctx=cls.MockContext(session_id), **kwargs) # type: ignore + return result + + @staticmethod + def _assert_activation_message(result: str, project_name: str, present: bool) -> None: + regex = r"^The project with name '" + project_name + r"'.*?is activated.$" + match = re.search(regex, result, re.MULTILINE) + if present: + assert match is not None, f"Expected project activation message in result:\n{result}" + else: + assert match is None, f"Expected no project activation message in result:\n{result}" + + @pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True) + def test_initial_instructions_provide_project_activation_message_once_per_session(self, serena_agent: SerenaAgent) -> None: + """ + Tests that the project activation message is provided on the first call to InitialInstructionsTool for a session, + but not on subsequent calls within the same session. #1372 + """ + project_name = "test_repo_python" + session1 = "session1" + session2 = "session2" + + result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) + self._assert_activation_message(result1, project_name, present=True) + + result2 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session2) + self._assert_activation_message(result2, project_name, present=True) + + result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) + self._assert_activation_message(result3, project_name, present=False) + + @pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True) + def test_dynamically_activated_mode_is_provided_once_per_session(self, serena_agent: SerenaAgent) -> None: + """ + Tests that when a new project is activated within a session that has a different mode configuration (e.g. no-onboarding), + the new mode's prompts are provided at project activation but not in subsequent initial instructions calls within the same + session, while they are provided in the initial instructions of a new session. + """ + project_name1 = "test_repo_python" + project_name2 = "test_repo_java" + session1 = "session1" + session2 = "session2" + + # the initial instructions must contain the project activation message for the first project + result1 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) + self._assert_activation_message(result1, project_name1, present=True) + + # now activate another project which dynamically enables a new mode (no-onboarding) + reg_project = serena_agent.serena_config.get_registered_project(project_name2) + reg_project.project_config.default_modes = ["no-onboarding"] + expected_new_mode_message = "The onboarding process is not applied." + result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name2, session_id=session1) + + # the new mode's prompt must be included in the activation message + self._assert_activation_message(result2, project_name2, present=True) + assert expected_new_mode_message in result2, ( + f"Expected new mode message '{expected_new_mode_message}' not found in result:\n{result2}" + ) + + # the mode prompt must not be included in subsequent calls to the initial instructions tool within the same session + result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1) + assert expected_new_mode_message not in result3, ( + f"Expected new mode message '{expected_new_mode_message}' to not be included in subsequent calls, but it was found in result:\n{result3}" + ) + + # the mode prompt must be included in the initial instructions of a new session + result4 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session2) + assert expected_new_mode_message in result4, ( + f"Expected new mode message '{expected_new_mode_message}' to be included in new session, but it was not found in result:\n{result4}" + ) + + # the initial instructions for the new session must also include the activation message for the project + self._assert_activation_message(result4, project_name2, present=True) + + @pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True) + def test_activate_project_tool_always_returns_activation_message(self, serena_agent: SerenaAgent) -> None: + project_name = "test_repo_python" + session = "session1" + + result1 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name, session_id=session) + self._assert_activation_message(result1, project_name, present=True) + + result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name, session_id=session) + self._assert_activation_message(result2, project_name, present=True) diff --git a/test/serena/test_symbol_editing.py b/test/serena/test_symbol_editing.py index d6760982..b6f8abf2 100644 --- a/test/serena/test_symbol_editing.py +++ b/test/serena/test_symbol_editing.py @@ -328,15 +328,6 @@ class InsertInRelToSymbolTest(EditingTest): @pytest.mark.parametrize( "test_case", [ - pytest.param( - InsertInRelToSymbolTest( - Language.PYTHON, - PYTHON_TEST_REL_FILE_PATH, - "typed_module_var", - NEW_PYTHON_VARIABLE, - ), - marks=pytest.mark.python, - ), pytest.param( InsertInRelToSymbolTest( Language.PYTHON, diff --git a/uv.lock b/uv.lock index 793e80cb..f5f91eff 100644 --- a/uv.lock +++ b/uv.lock @@ -190,23 +190,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" }, ] -[[package]] -name = "bleach" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2" }, -] - [[package]] name = "blinker" version = "1.9.0" @@ -571,15 +554,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - [[package]] name = "dict2css" version = "0.3.0.post1" @@ -1274,15 +1248,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, -] - [[package]] name = "latexcodec" version = "3.0.1" @@ -1461,15 +1426,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mistune" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, -] - [[package]] name = "more-itertools" version = "10.8.0" @@ -1636,31 +1592,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, ] -[[package]] -name = "nbconvert" -version = "7.17.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, - { name = "defusedxml" }, - { name = "jinja2" }, - { name = "jupyter-core" }, - { name = "jupyterlab-pygments" }, - { name = "markupsafe" }, - { name = "mistune" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pandocfilters" }, - { name = "pygments" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, -] - [[package]] name = "nbformat" version = "5.10.4" @@ -1676,23 +1607,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, ] -[[package]] -name = "nbsphinx" -version = "0.9.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "sphinx" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/84/b1856b7651ac34e965aa567a158714c7f3bd42a1b1ce76bf423ffb99872c/nbsphinx-0.9.7.tar.gz", hash = "sha256:abd298a686d55fa894ef697c51d44f24e53aa312dadae38e82920f250a5456fe", size = 180479, upload-time = "2025-03-03T19:46:08.069Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/2d/8c8e635bcc6757573d311bb3c5445426382f280da32b8cd6d82d501ef4a4/nbsphinx-0.9.7-py3-none-any.whl", hash = "sha256:7292c3767fea29e405c60743eee5393682a83982ab202ff98f5eb2db02629da8", size = 31660, upload-time = "2025-03-03T19:46:06.581Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -1729,15 +1643,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] -[[package]] -name = "pandocfilters" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, -] - [[package]] name = "parso" version = "0.8.6" @@ -2999,8 +2904,6 @@ dev = [ { name = "jinja2" }, { name = "jupyter-book" }, { name = "mypy" }, - { name = "nbconvert" }, - { name = "nbsphinx" }, { name = "poethepoet" }, { name = "pyasn1" }, { name = "pyinstrument" }, @@ -3043,8 +2946,6 @@ requires-dist = [ { name = "lsprotocol", specifier = "==2025.0.0" }, { name = "mcp", specifier = "==1.26.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.17.0" }, - { name = "nbconvert", marker = "extra == 'dev'", specifier = "==7.17.0" }, - { name = "nbsphinx", marker = "extra == 'dev'", specifier = "==0.9.7" }, { name = "overrides", specifier = "==7.7.0" }, { name = "pathspec", specifier = "==0.12.1" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = "==0.36.0" }, @@ -3647,18 +3548,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] -[[package]] -name = "tinycss2" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, -] - [[package]] name = "toml-sort" version = "0.24.2"