Files
serena/scripts/demo_progressive_tool_shortening.py
T
Dominik Jain 6707cd9b7e Introduce component-based licensing: Serena GPL-3.0-or-later, SolidLSP MIT
The repository is licensed per component. SolidLSP (src/solidlsp,
test/solidlsp, test/resources) remains MIT-licensed and independently
reusable; the Serena application (src/serena, src/interprompt, scripts,
test/serena, docs) is licensed under GPL-3.0-or-later starting with the v2
licensing transition. The change is not retroactive: all releases and
commits up to v1.7.0 / 74c38a65 (tag mit-final) remain available under MIT.

Since MIT is GPL-compatible, a distribution combining both (such as the
serena-agent package) is as a whole subject to GPL-3.0-or-later, while the
SolidLSP files themselves stay MIT and can be extracted and used separately
under MIT terms. The distribution metadata therefore declares
GPL-3.0-or-later, with both license texts shipped alongside it.

Serena originally began under the GPL (v2) and was switched to MIT in
May 2025 following community requests. We consider that change a mistake;
the substantial changes in v2 make this the appropriate time to revert it.
We want the best version of Serena to remain free.

Changes:
* LICENSE is now the licensing overview; canonical license texts live in
  LICENSES/ (MIT.txt is the previous LICENSE verbatim, GPL-3.0-or-later.txt
  is the unmodified FSF text)
* pyproject.toml declares the PEP 639 license expression
  "GPL-3.0-or-later" and bundles LICENSE and LICENSES/* as license files;
  the deprecated MIT classifier is dropped and flake.nix declares gpl3Plus;
  README has per-component license badges and a License section
* SPDX-License-Identifier headers in all Python sources under src/ and
  scripts/, added by the new idempotent scripts/add_spdx_headers.py, which
  gen_prompt_factory.py also uses to keep the header on the generated
  module; existing third-party notices are preserved
* CLA.md: Contributor License Agreement (contributor retains copyright;
  grants a perpetual, irrevocable license including relicensing under any
  terms, incl. proprietary/commercial; patent grant; authority
  representations), to be enforced repository-wide via cla-assistant.io
* CONTRIBUTING.md, PR template and a new docs page explain the licensing
  boundary and the CLA workflow
2026-09-14 21:33:18 +02:00

193 lines
6.7 KiB
Python

"""
Demonstrates the progressive shortening of tool results when max_answer_chars is exceeded.
It exercises all tools that use _limit_length with shortened_results,
printing the full result, then progressively tighter max_answer_chars
to show the successive shortening stages. Both LSP and JetBrains backends
are tested (JB is skipped if no IDE is running).
"""
# SPDX-License-Identifier: GPL-3.0-or-later
import json
from pprint import pprint
from serena.agent import SerenaAgent
from serena.config.serena_config import LanguageBackend, SerenaConfig
from serena.constants import REPO_ROOT
from serena.tools import (
FindReferencingSymbolsTool,
FindSymbolTool,
GetSymbolsOverviewTool,
JetBrainsFindReferencingSymbolsTool,
JetBrainsFindSymbolTool,
JetBrainsGetSymbolsOverviewTool,
SearchForPatternTool,
)
SEPARATOR = "=" * 80
# symbol with many references across multiple files, good for testing shortening
REF_SYMBOL = "_limit_length"
REF_FILE = "src/serena/tools/tools_base.py"
# file with many symbols, good for testing overview shortening
OVERVIEW_FILE = "src/serena/tools/tools_base.py"
def run_with_shrinking(agent: SerenaAgent, label: str, fn, char_limits: list[int]) -> None:
"""Run a tool call at several max_answer_chars limits and print results."""
for limit in char_limits:
tag = f"{label} (max_answer_chars={limit})"
print(f"\n{SEPARATOR}")
print(tag)
print(SEPARATOR)
result = agent.execute_task(lambda lim=limit: fn(lim))
n = len(result)
print(f"[length={n}]")
try:
pprint(json.loads(result), width=200)
except (json.JSONDecodeError, ValueError):
print(result)
def run_lsp_tools(agent: SerenaAgent) -> None:
print("\n\n### LSP BACKEND ###\n")
# LSP: FindReferencingSymbolsTool — three shortening stages:
# 1. refs without context lines 2. per-file counts 3. total summary
lsp_refs = agent.get_tool(FindReferencingSymbolsTool)
run_with_shrinking(
agent,
"LSP FindReferencingSymbolsTool",
lambda lim: lsp_refs.apply(REF_SYMBOL, REF_FILE, max_answer_chars=lim),
char_limits=[50000, 3000, 500, 200],
)
# LSP: FindSymbolTool — one shortening stage: names with kind only
lsp_find = agent.get_tool(FindSymbolTool)
run_with_shrinking(
agent,
"LSP FindSymbolTool (depth=1)",
lambda lim: lsp_find.apply("Tool", relative_path=REF_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 200],
)
# LSP: FindSymbolTool with max_matches exceeded — tests the early-return shortened path
print(f"\n{SEPARATOR}")
print("LSP FindSymbolTool (max_matches=1, broad search)")
print(SEPARATOR)
result = agent.execute_task(lambda: lsp_find.apply("apply", max_matches=1))
print(f"[length={len(result)}]")
print(result)
# LSP: GetSymbolsOverviewTool — two shortening stages for depth>0:
# 1. depth-0 overview 2. counts by kind
# one stage for depth==0: counts by kind
lsp_overview = agent.get_tool(GetSymbolsOverviewTool)
run_with_shrinking(
agent,
"LSP GetSymbolsOverviewTool (depth=1)",
lambda lim: lsp_overview.apply(OVERVIEW_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
run_with_shrinking(
agent,
"LSP GetSymbolsOverviewTool (depth=0)",
lambda lim: lsp_overview.apply(OVERVIEW_FILE, depth=0, max_answer_chars=lim),
char_limits=[50000, 200],
)
def run_backend_independent_tools(agent: SerenaAgent) -> None:
print("\n\n### BACKEND-INDEPENDENT TOOLS ###\n")
# SearchForPatternTool — three shortening stages:
# 1. match lines per file (no context) 2. match counts per file 3. total summary
search_tool = agent.get_tool(SearchForPatternTool)
run_with_shrinking(
agent,
"SearchForPatternTool (with context)",
lambda lim: search_tool.apply(
"_limit_length",
context_lines_before=1,
context_lines_after=1,
relative_path="src/serena/tools",
max_answer_chars=lim,
),
char_limits=[50000, 1000, 200],
)
def run_jb_tools(agent: SerenaAgent) -> None:
print("\n\n### JETBRAINS BACKEND ###\n")
# JB: FindReferencingSymbolsTool — two shortening stages:
# 1. per-file counts 2. total summary
jb_refs = agent.get_tool(JetBrainsFindReferencingSymbolsTool)
run_with_shrinking(
agent,
"JB FindReferencingSymbolsTool",
lambda lim: jb_refs.apply(REF_SYMBOL, REF_FILE, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
# JB: FindSymbolTool — one shortening stage: names with kind only
jb_find = agent.get_tool(JetBrainsFindSymbolTool)
run_with_shrinking(
agent,
"JB FindSymbolTool (depth=1)",
lambda lim: jb_find.apply("Tool", relative_path=REF_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 200],
)
# JB: FindSymbolTool with max_matches exceeded — tests the early-return shortened path
print(f"\n{SEPARATOR}")
print("JB FindSymbolTool (max_matches=1, broad search)")
print(SEPARATOR)
result = agent.execute_task(lambda: jb_find.apply("apply", max_matches=1))
print(f"[length={len(result)}]")
print(result)
# JB: GetSymbolsOverviewTool — two shortening stages for depth>0:
# 1. depth-0 overview 2. counts by type
# two stages for depth==0: grouped symbols, then counts by type
jb_overview = agent.get_tool(JetBrainsGetSymbolsOverviewTool)
run_with_shrinking(
agent,
"JB GetSymbolsOverviewTool (depth=1)",
lambda lim: jb_overview.apply(OVERVIEW_FILE, depth=1, max_answer_chars=lim),
char_limits=[50000, 500, 200],
)
run_with_shrinking(
agent,
"JB GetSymbolsOverviewTool (depth=0)",
lambda lim: jb_overview.apply(OVERVIEW_FILE, depth=0, max_answer_chars=lim),
char_limits=[50000, 200],
)
def make_agent(backend: LanguageBackend) -> SerenaAgent:
config = SerenaConfig.from_config_file()
config.web_dashboard = False
config.language_backend = backend
return SerenaAgent(project=REPO_ROOT, serena_config=config)
if __name__ == "__main__":
# LSP backend
lsp_agent = make_agent(LanguageBackend.LSP)
try:
run_lsp_tools(lsp_agent)
run_backend_independent_tools(lsp_agent)
finally:
lsp_agent.on_shutdown()
# JetBrains backend (requires a running IDE)
try:
jb_agent = make_agent(LanguageBackend.JETBRAINS)
try:
run_jb_tools(jb_agent)
finally:
jb_agent.on_shutdown()
except Exception as e:
print(f"\nJetBrains backend not available, skipping: {e}")