mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 06:15:43 +00:00
Merge pull request #1509 from oraios/agent-config-update-events
Introduce events for agent config/status updates
This commit is contained in:
7 files changed
+80
-11
No files matched your search
@@ -13,6 +13,9 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* Dashboard:
|
||||
- Make list of trusted hosts configurable, fixing host validation introduced in v1.5.2 allowing only
|
||||
default local hostnames, effectively preventing remote connections
|
||||
- Decouple configuration computation from the agent's task queue by introducing events for agent config/status updates.
|
||||
This allows the dashboard to display the configuration while the project provided at startup is still initialising. #1064
|
||||
- Fix empty executions queue displaying "Loading..."
|
||||
|
||||
# v1.5.3 (2026-05-26)
|
||||
|
||||
|
||||
+39
-6
@@ -544,6 +544,7 @@ class SerenaAgent:
|
||||
self._project_prompt_status = ProjectPromptProvisionStatus()
|
||||
self._session_mode_selection_definition = modes
|
||||
self.version = serena_version()
|
||||
self._config_changed_callbacks: list[Callable[[], None]] = []
|
||||
|
||||
# obtain serena configuration using the decoupled factory function
|
||||
self.serena_config = serena_config or SerenaConfig.from_config_file()
|
||||
@@ -633,7 +634,7 @@ class SerenaAgent:
|
||||
|
||||
# create executor for starting the language server and running tools in another thread
|
||||
# This executor is used to achieve linear task execution
|
||||
self._task_executor = TaskExecutor("SerenaAgentTaskExecutor")
|
||||
self._task_executor = TaskExecutor("SerenaAgentTaskExecutor", self._task_completion_callback)
|
||||
|
||||
# Initialize the prompt factory
|
||||
self.prompt_factory = SerenaPromptFactory()
|
||||
@@ -659,18 +660,29 @@ class SerenaAgent:
|
||||
self._active_tools: AvailableTools
|
||||
self._update_active_tools()
|
||||
|
||||
# start the dashboard (web frontend), registering its log handler
|
||||
# should be the last thing to happen in the initialization since the dashboard
|
||||
# may access various parts of the agent
|
||||
# create the dashboard backend (if enabled), which will register callback.
|
||||
dashboard_api: SerenaDashboardAPI | None = None
|
||||
if self.serena_config.web_dashboard:
|
||||
self._dashboard_thread, port = SerenaDashboardAPI(
|
||||
dashboard_api = SerenaDashboardAPI(
|
||||
get_memory_log_handler(),
|
||||
tool_names,
|
||||
agent=self,
|
||||
tool_usage_stats=self._tool_usage_stats,
|
||||
host=self.serena_config.web_dashboard_listen_address,
|
||||
trusted_hosts=self.serena_config.web_dashboard_trusted_hosts,
|
||||
).run_in_thread()
|
||||
)
|
||||
|
||||
# propagate the initial config/state to listeners, particularly to the dashboard API.
|
||||
# Note: The only ongoing task can be the LS initialisation, which does not change the config
|
||||
# This is executed before starting the dashboard thread to ensure that the dashboard
|
||||
# has the correct initial state before requests can come in.
|
||||
self._on_config_changed()
|
||||
|
||||
# start the dashboard backend thread and the dashboard frontend manager (if enabled).
|
||||
# This should be the last thing to happen in the initialization since the dashboard
|
||||
# may access various parts of the agent
|
||||
if dashboard_api:
|
||||
dashboard_thread, port = dashboard_api.run_in_thread()
|
||||
self._dashboard_manager = DashboardManager(
|
||||
port,
|
||||
self.serena_config.web_dashboard_listen_address,
|
||||
@@ -1077,6 +1089,27 @@ class SerenaAgent:
|
||||
"""
|
||||
return self._task_executor.execute_task(task, name=name, logged=logged, timeout=timeout)
|
||||
|
||||
def _task_completion_callback(self) -> None:
|
||||
"""
|
||||
Called after every task completion. Tasks potentially change the agent's state/configuration.
|
||||
"""
|
||||
self._on_config_changed()
|
||||
|
||||
def _on_config_changed(self) -> None:
|
||||
for callback in self._config_changed_callbacks:
|
||||
try:
|
||||
callback()
|
||||
except Exception as e:
|
||||
log.error(f"Error in config changed callback {callback}: {e}", exc_info=e)
|
||||
|
||||
def register_config_changed_callback(self, callback: Callable[[], None]) -> None:
|
||||
"""
|
||||
Registers a callback to be called when the agent configuration has (potentially) changed.
|
||||
|
||||
:param callback: the callback function to register
|
||||
"""
|
||||
self._config_changed_callbacks.append(callback)
|
||||
|
||||
def is_using_language_server(self) -> bool:
|
||||
"""
|
||||
:return: whether this agent uses language server-based code analysis
|
||||
|
||||
+14
-4
@@ -212,7 +212,12 @@ class SerenaDashboardAPI:
|
||||
self._news_ready = threading.Event()
|
||||
self._setup_routes()
|
||||
self._read_news = ReadNews.load()
|
||||
# Fetch remote news in background on startup (non-blocking)
|
||||
|
||||
# register callback for config changes
|
||||
self._current_config_overview: dict[str, Any] | None = None
|
||||
self._agent.register_config_changed_callback(self._on_agent_config_changed)
|
||||
|
||||
# fetch remote news in background on startup (non-blocking)
|
||||
threading.Thread(target=self._fetch_news, daemon=True).start()
|
||||
|
||||
@property
|
||||
@@ -277,8 +282,10 @@ class SerenaDashboardAPI:
|
||||
|
||||
@self._app.route("/get_config_overview", methods=["GET"])
|
||||
def get_config_overview() -> dict[str, Any]:
|
||||
result = self._agent.execute_task(self._get_config_overview, logged=False)
|
||||
return result.model_dump()
|
||||
result = self._current_config_overview
|
||||
if result is None:
|
||||
raise ValueError("Config overview not yet available")
|
||||
return result
|
||||
|
||||
@self._app.route("/shutdown", methods=["PUT"])
|
||||
def shutdown() -> dict[str, str]:
|
||||
@@ -473,7 +480,7 @@ class SerenaDashboardAPI:
|
||||
if self._tool_usage_stats is not None:
|
||||
self._tool_usage_stats.clear()
|
||||
|
||||
def _get_config_overview(self) -> ResponseConfigOverview:
|
||||
def _compute_config_overview(self) -> ResponseConfigOverview:
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.tools.tools_base import Tool
|
||||
|
||||
@@ -601,6 +608,9 @@ class SerenaDashboardAPI:
|
||||
serena_version=self._agent.version,
|
||||
)
|
||||
|
||||
def _on_agent_config_changed(self) -> None:
|
||||
self._current_config_overview = self._compute_config_overview().model_dump()
|
||||
|
||||
def _get_available_languages(self) -> ResponseAvailableLanguages:
|
||||
from solidlsp.ls_config import Language
|
||||
|
||||
|
||||
@@ -1012,6 +1012,11 @@ code, pre, kbd, samp,
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loaded {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--log-error);
|
||||
text-align: center;
|
||||
|
||||
@@ -944,6 +944,7 @@ class Dashboard {
|
||||
|
||||
displayActiveExecutionsQueue(executions) {
|
||||
if (!executions || executions.length === 0) {
|
||||
this.$activeExecutionQueueDisplay.html('<div class="loaded">No executions queued.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ T = TypeVar("T")
|
||||
|
||||
|
||||
class TaskExecutor:
|
||||
def __init__(self, name: str):
|
||||
def __init__(self, name: str, task_completion_callback: Callable[[], None] | None = None):
|
||||
"""
|
||||
:param name: the name of the task executor, used for logging purposes
|
||||
:param task_completion_callback: an optional callback function that will be called after each task completion
|
||||
(regardless of success, failure, or cancellation)
|
||||
"""
|
||||
self._task_executor_lock = threading.Lock()
|
||||
self._task_executor_queue: list[TaskExecutor.Task] = []
|
||||
self._task_executor_thread = Thread(target=self._process_task_queue, name=name, daemon=True)
|
||||
@@ -24,6 +29,7 @@ class TaskExecutor:
|
||||
self._task_executor_task_index = 1
|
||||
self._task_executor_current_task: TaskExecutor.Task | None = None
|
||||
self._task_executor_last_executed_task_info: TaskExecutor.TaskInfo | None = None
|
||||
self._task_completion_callback = task_completion_callback
|
||||
|
||||
class Task(ToStringMixin, Generic[T]):
|
||||
def __init__(self, function: Callable[[], T], name: str, logged: bool = True, timeout: float | None = None):
|
||||
@@ -130,6 +136,13 @@ class TaskExecutor:
|
||||
if task.logged:
|
||||
self._task_executor_last_executed_task_info = self.TaskInfo.from_task(task, is_running=False)
|
||||
|
||||
# call the task completion callback if provided
|
||||
if self._task_completion_callback is not None:
|
||||
try:
|
||||
self._task_completion_callback()
|
||||
except Exception as e:
|
||||
log.error(f"Error in task completion callback after executing {task.name}: {e}", exc_info=e)
|
||||
|
||||
@dataclass
|
||||
class TaskInfo:
|
||||
name: str
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
|
||||
from serena.dashboard import SerenaDashboardAPI
|
||||
@@ -16,6 +17,9 @@ class _DummyAgent:
|
||||
def __init__(self, project: SimpleNamespace | None) -> None:
|
||||
self._project = project
|
||||
|
||||
def register_config_changed_callback(self, callback: Callable[[], None]) -> None:
|
||||
pass
|
||||
|
||||
def execute_task(self, func, *, logged: bool | None = None, name: str | None = None):
|
||||
del logged, name
|
||||
return func()
|
||||
|
||||
Reference in new issue
Block a user