diff --git a/.github/AGENTS.md b/.github/AGENTS.md index 5a9c3686b..105dd8ff3 100644 --- a/.github/AGENTS.md +++ b/.github/AGENTS.md @@ -9,16 +9,13 @@ - `workflows/` contains GitHub Actions workflow definitions. - `scripts/` contains Python helpers called by workflows. -- This file owns release automation rules; user-facing release documentation belongs under `docs/`. +- Root-level release rules remain in the root `AGENTS.md`; this file owns automation-specific details. ## Local Contracts - Docker publishing lives in `workflows/docker-publish.yml` and delegates planning to `scripts/docker_release_plan.py`. - Releasable tags are `vX.Y` tags at or above `v1.0`, matching the workflow environment. -- On `main`, the newest eligible tag publishes both the version tag and `latest`, then creates or updates its GitHub release after the image push succeeds; other allowed branches publish only their branch tag. -- Manual dispatch without a tag backfills missing Docker Hub tags. Manual dispatch with a tag rebuilds that target and refreshes `latest` and the GitHub release only when it remains the newest eligible tag on `main`. - Release-note generation reads `scripts/openrouter_release_notes_system_prompt.md` from the repository root and requires OpenRouter credentials from workflow environment variables. -- Release notes compare against the previous published GitHub release tag and fall back to `No release notes.` when no meaningful summary is generated. - Keep workflow secrets in GitHub Actions secrets or environment variables. Do not commit credentials, tokens, or generated release bodies containing private data. - Workflow scripts must fail loudly with actionable messages when required environment variables or git refs are missing. @@ -26,7 +23,7 @@ - Prefer deterministic, testable Python for workflow planning logic instead of complex inline shell in YAML. - Preserve manual dispatch behavior when changing Docker publishing. -- Keep branch, tag, and release behavior synchronized between workflow YAML, release scripts, tests, and user-facing release documentation. +- Keep branch, tag, and release behavior synchronized between workflow YAML, release scripts, tests, and root documentation. ## Verification diff --git a/AGENTS.md b/AGENTS.md index 704ab7554..31ab6a620 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,83 +1,364 @@ -# Agent Zero DOX +# Agent Zero - AGENTS.md -## Purpose +[Generated using reconnaissance on 2026-02-22] -- Own project-wide engineering rules and the top-level DOX index. -- Keep detailed contracts in the closest applicable child `AGENTS.md`. +## Quick Reference +Tech Stack: Framework Python 3.12+ | Agent execution Python 3.13 in Docker | Flask | Alpine.js | LiteLLM | WebSocket (Socket.io) +Dev Server: python run_ui.py (discover host/port from startup output or runtime configuration; do not assume a default port) +Run Tests: pytest (standard) or pytest tests/test_name.py (file-scoped) +Documentation: README.md | docs/ +Frontend & Plugin DOX: [WebUI](webui/AGENTS.md) | [Components](webui/components/AGENTS.md) | [Frontend JS](webui/js/AGENTS.md) | [Plugins](plugins/AGENTS.md) -## Project +--- -- Stack: Python 3.12+ framework, Python 3.13 agent execution runtime, Flask, Alpine.js, LiteLLM, and Socket.IO. -- Start the WebUI with `python run_ui.py`; discover its URL from startup output, Docker mappings, or explicit configuration rather than assuming a port. -- Run the full test suite with `pytest` or a focused file with `pytest tests/test_name.py`. -- Human-facing documentation lives in `README.md` and `docs/`. +## Table of Contents +1. [Project Overview](#project-overview) +2. [Core Commands](#core-commands) +3. [Docker Environment](#docker-environment) +4. [Project Structure](#project-structure) +5. [Development Patterns & Conventions](#development-patterns--conventions) +6. [Safety and Permissions](#safety-and-permissions) +7. [Code Examples](#code-examples) +8. [Git Workflow](#git-workflow) +9. [Release Notes](#release-notes) +10. [Troubleshooting](#troubleshooting) -## Root Ownership +--- -- `agent.py` owns `Agent`, `AgentContext`, and loop data. -- `initialize.py` owns framework initialization. -- `models.py` owns model-provider configuration and LiteLLM integration. -- `run_ui.py` is the WebUI entry point. -- `DockerfileLocal` must remain compatible with the contracts under `docker/`. -- Runtime or user state under `usr/` and `tmp/` is intentionally outside tracked DOX unless the user explicitly asks otherwise. +## Project Overview -## Project-Wide Contracts +Agent Zero is a dynamic, organic agentic framework designed to grow and learn. It uses the operating system as a tool, featuring a multi-agent cooperation model where every agent can create subordinates to break down tasks. -- Import `AgentContext` and `AgentContextType` from `agent`, not `helpers.context`. -- Never commit secrets, `.env` files, API keys, tokens, or private user data. -- Preserve authentication and CSRF protections. -- Use Linux paths and commands in examples. -- Treat the Docker container exposed at `localhost:32080` as the live plugin/backend runtime when that target is named. -- Copy live core-plugin changes back into tracked source under `plugins/`. -- Develop new custom plugins under ignored `usr/plugins/`; tracked bundled plugins live under `plugins/`. -- Use the framework runtime for backend and plugin-hook verification, not the separate agent execution runtime. +Type: Full-Stack Agentic Framework (Python Backend + Alpine.js Frontend) +Status: Active Development +Primary Language(s): Python, JavaScript (ES Modules) -## Permissions +--- -Allowed without asking: +## Core Commands -- Read repository files. -- Update files under `usr/`. +### Setup +Do not combine these commands; run them individually: +```bash +pip install -r requirements.txt +``` +- Start WebUI: python run_ui.py +- Discover the WebUI URL from startup output, launcher/Docker port mappings, or explicit `--host`/`--port`/`WEB_UI_PORT` configuration; do not hardcode a default port. -Ask before: +--- -- Installing dependencies. -- Deleting core files outside `usr/` or `tmp/`. -- Modifying `agent.py` or `initialize.py`. -- Creating commits or pushing branches. +## Docker Environment -## DOX Workflow +When running in Docker, Agent Zero uses two distinct Python runtimes to isolate the framework itself from code executed on behalf of the agent: -- `AGENTS.md` files are binding contracts for their subtrees. -- Before editing, read this file and every `AGENTS.md` on the path to each target; the closest contract controls local details without weakening parent rules. -- Keep work understandable from the applicable DOX chain. Put project-wide rules here and concrete ownership, workflows, inputs, outputs, side effects, and verification in child docs. -- Create a child `AGENTS.md` only for a durable boundary with distinct ownership or workflow. -- Child docs should use: Purpose, Ownership, Local Contracts, Work Guidance, Verification, and Child DOX Index. -- After every meaningful change, re-check the affected paths, update the closest owning docs and indexes, remove stale guidance, and run relevant verification. -- Do not document ignored `usr/` or `tmp/` changes unless explicitly requested. -- Keep DOX concise, current, operational, and free of diary entries or duplicated parent guidance. +### 1. Framework Runtime (/opt/venv-a0) +- Version: Python 3.12.4 +- Purpose: Runs the Agent Zero framework itself: WebUI backend, API, core loop, scheduler, framework imports, and plugin hooks/tools that execute inside the framework process. +- Packages: Contains framework dependencies from requirements.txt. +- Verification: Use this runtime for framework/backend import checks, WebUI startup checks, and plugin hook behavior unless the code explicitly switches environments. + +### 2. Agent Execution Runtime (/opt/venv) +- Version: Python 3.13 +- Purpose: Default Python environment for the agent's terminal/code-execution tasks and user code run by the agent. +- Behavior: Packages installed by the agent during a task belong here so task dependencies do not pollute or prove the framework runtime. Do not use this runtime as evidence that framework imports, WebUI startup, or plugin hooks work. + +--- + +## Project Structure + +``` +/ +├── agent.py # Core Agent and AgentContext definitions +├── initialize.py # Framework initialization logic +├── models.py # LLM provider configurations +├── run_ui.py # WebUI server entry point +├── api/ # API Handlers (ApiHandler subclasses) + WsHandler subclasses (ws_*.py) +├── extensions/ # Backend lifecycle extensions +├── helpers/ # Shared Python utilities (plugins, files, etc.) +├── tools/ # Agent tools (Tool subclasses) +├── webui/ +│ ├── components/ # Alpine.js components +│ ├── js/ # Core frontend logic (modals, stores, etc.) +│ └── index.html # Main UI shell +├── usr/ # User data directory (isolated from core) +│ ├── plugins/ # Custom user plugins +│ ├── settings.json # User-specific configuration +│ └── workdir/ # Default agent workspace +├── plugins/ # Core system plugins +├── agents/ # Agent profiles (prompts and config) +├── prompts/ # System and message prompt templates +├── knowledge/ +│ └── main/about/ # Agent self-knowledge reference material +│ ├── identity.md # Philosophy, principles, project context +│ ├── architecture.md # Agent loop, multi-agent coordination, extensions +│ ├── capabilities.md # Detailed capabilities and limitations +│ ├── configuration.md # LLM roles, providers, profiles, plugins, settings +│ └── setup-and-deployment.md # Docker deployment, updates, troubleshooting +└── tests/ # Pytest suite +``` + +Key Files: +- agent.py: Defines AgentContext, LoopData virtual prompt areas (Protocol before history and Extras after history), and the main Agent class. +- helpers/plugins.py: Plugin discovery and configuration logic. +- webui/js/AlpineStore.js: Store factory for reactive frontend state. +- helpers/api.py: Base class for all API endpoints. +- models.py: LLM provider configuration and LiteLLM wrappers; framework LiteLLM defaults such as `drop_params=True` are merged with `litellm_global_kwargs`, configured values override framework defaults, documented module-level switches such as `drop_params` are applied to LiteLLM, and merged kwargs are passed per call. +- scripts/openrouter_release_notes_system_prompt.md: Editable system prompt used to generate GitHub release notes during Docker publishing. +- knowledge/main/about/: Agent self-knowledge files. Not user-facing docs - written for the agent's internal reference. +- webui/components/AGENTS.md: DOX contract for Alpine component architecture. +- webui/js/AGENTS.md: DOX contract for frontend infrastructure, modal stack, API helpers, and extension loading. +- plugins/AGENTS.md: DOX contract for bundled and custom plugin architecture; `usr/plugins/` remains ignored user state. + +--- + +## Development Patterns & Conventions + +### Backend (Python) +- Context Access: Use from agent import AgentContext, AgentContextType (not helpers.context). +- Communication: Use mq from helpers.messages to log proactive UI messages: + mq.log_user_message(context.id, "Message", source="Plugin") +- API Handlers: Derive from ApiHandler in helpers/api.py. +- Extensions: Use the extension framework in helpers/extension.py for lifecycle hooks. +- Error Handling: Use RepairableException for errors the LLM might be able to fix. + +### Frontend (Alpine.js) +- Store Gating: Always wrap store-dependent content in a template: +```html +
+
+
+
@@ -263,6 +268,17 @@ It is not a replacement for Git or backups. It is a practical safety layer for t
- **Client/project isolation:** keep memory, secrets, instructions, files, and model choices separated by project.
- **Scheduled operations:** run recurring checks and monitoring tasks with project-scoped context and credentials.
+## Safety Model
+
+Agent Zero is powerful because it can use a real environment.
+
+- Keep it running inside Docker or another isolated environment.
+- Do not mount your entire home directory unless you understand the risk.
+- Grant A0 CLI Read+Write access and remote code execution only for machines and workspaces you trust.
+- Store credentials in project secrets or settings, not in prompts or public files.
+- Review actions that touch accounts, money, production systems, or private data.
+- Keep backups for important workspaces.
+
## Documentation
| I want to... | Start here |
@@ -298,16 +314,3 @@ You can help by improving docs, creating skills, publishing plugins, testing mod
- [YouTube](https://www.youtube.com/@AgentZeroFW) for demos and tutorials.
- [X](https://x.com/Agent0ai), [LinkedIn](https://www.linkedin.com/company/109758317), and [Warpcast](https://warpcast.com/agent-zero) for updates.
- [GitHub Issues](https://github.com/agent0ai/agent-zero/issues) for bugs and feature requests.
-
-[Space Agent](https://github.com/agent0ai/space-agent) is the related, more polished product direction for the agent-shaped workspace. Agent Zero remains the open framework and Linux-powered workbench.
-
-## Safety Model
-
-Agent Zero is powerful because it can use a real environment.
-
-- Keep it running inside Docker or another isolated environment.
-- Do not mount your entire home directory unless you understand the risk.
-- Grant A0 CLI Read+Write access and remote code execution only for machines and workspaces you trust.
-- Store credentials in project secrets or settings, not in prompts or public files.
-- Review actions that touch accounts, money, production systems, or private data.
-- Keep backups for important workspaces.
diff --git a/api/settings_get.py.dox.md b/api/settings_get.py.dox.md
index 46b2ccdd7..e584283fd 100644
--- a/api/settings_get.py.dox.md
+++ b/api/settings_get.py.dox.md
@@ -28,7 +28,6 @@
## Key Concepts
- Important called helpers/classes observed in the source: `settings.get_settings`, `settings.convert_out`.
-- The returned settings map includes normalized `ui_control_visibility` mobile and desktop flags for the configurable WebUI controls.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
diff --git a/api/settings_set.py.dox.md b/api/settings_set.py.dox.md
index 0d3e62e6c..0f08f9862 100644
--- a/api/settings_set.py.dox.md
+++ b/api/settings_set.py.dox.md
@@ -26,7 +26,6 @@
## Key Concepts
- Important called helpers/classes observed in the source: `settings.convert_in`, `settings.set_settings`, `settings.convert_out`, `settings.Settings`.
-- The settings payload accepts normalized `ui_control_visibility` mobile and desktop flags and returns the persisted map with the rest of the settings.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
diff --git a/conf/model_providers.yaml b/conf/model_providers.yaml
index 5e90efa3b..541a4888b 100644
--- a/conf/model_providers.yaml
+++ b/conf/model_providers.yaml
@@ -84,7 +84,6 @@ chat:
endpoint_url: "/v1/models"
default_base: "http://host.docker.internal:1234"
kwargs:
- a0_api_mode: chat
api_base: "http://host.docker.internal:1234/v1"
api_key: "lm-studio"
llama_cpp:
@@ -94,7 +93,6 @@ chat:
endpoint_url: "/v1/models"
default_base: "http://host.docker.internal:8080"
kwargs:
- a0_api_mode: chat
api_base: "http://host.docker.internal:8080/v1"
api_key: "llama-cpp"
mistral:
@@ -114,11 +112,6 @@ chat:
endpoint_url: "/models"
kwargs:
api_base: https://api.tokenfactory.nebius.com/v1
- nvidia_nim:
- name: NVIDIA NIM
- litellm_provider: nvidia_nim
- models_list:
- endpoint_url: "https://integrate.api.nvidia.com/v1/models"
ollama:
name: Ollama
litellm_provider: ollama
@@ -127,7 +120,6 @@ chat:
format: "ollama"
default_base: "http://host.docker.internal:11434"
kwargs:
- a0_api_mode: chat
api_base: "http://host.docker.internal:11434"
omlx:
name: oMLX
@@ -136,7 +128,6 @@ chat:
endpoint_url: "/v1/models"
default_base: "http://host.docker.internal:8000"
kwargs:
- a0_api_mode: chat
api_base: "http://host.docker.internal:8000/v1"
api_key: "omlx"
ollama_cloud:
@@ -145,7 +136,6 @@ chat:
models_list:
endpoint_url: "/models"
kwargs:
- a0_api_mode: chat
api_base: https://ollama.com/v1
openai:
name: OpenAI
@@ -183,7 +173,6 @@ chat:
models_list:
endpoint_url: "https://api.venice.ai/api/v1/models"
kwargs:
- a0_api_mode: chat
api_base: https://api.venice.ai/api/v1
venice_parameters:
include_venice_system_prompt: false
@@ -194,7 +183,6 @@ chat:
endpoint_url: "/v1/models"
default_base: "http://host.docker.internal:8000"
kwargs:
- a0_api_mode: chat
api_base: "http://host.docker.internal:8000/v1"
api_key: "vllm"
xai:
@@ -219,8 +207,6 @@ chat:
other:
name: Other OpenAI compatible
litellm_provider: openai
- kwargs:
- a0_api_mode: chat
embedding:
huggingface:
@@ -244,11 +230,6 @@ embedding:
mistral:
name: Mistral AI
litellm_provider: mistral
- nvidia_nim:
- name: NVIDIA NIM
- litellm_provider: nvidia_nim
- models_list:
- endpoint_url: "https://integrate.api.nvidia.com/v1/models"
ollama:
name: Ollama
litellm_provider: ollama
diff --git a/docker/AGENTS.md b/docker/AGENTS.md
index 8fafcaaf5..757608953 100644
--- a/docker/AGENTS.md
+++ b/docker/AGENTS.md
@@ -13,8 +13,7 @@
## Local Contracts
-- Preserve the two-runtime model: the Python 3.12 framework runtime under `/opt/venv-a0` runs the WebUI, APIs, scheduler, framework imports, and plugin hooks; the Python 3.13 agent execution runtime under `/opt/venv` runs agent terminal tasks and user code.
-- Verify backend imports and plugin hooks with `/opt/venv-a0`; packages installed into `/opt/venv` do not prove framework compatibility.
+- Preserve the two-runtime model documented in the root contract: framework runtime under `/opt/venv-a0` and agent execution runtime under `/opt/venv`.
- Do not bake secrets, local `.env` values, or user data into images.
- Keep compose mounts aligned with `usr/`, `logs/`, and other runtime-state expectations.
- Image changes that affect GitHub publishing must stay synchronized with `.github/workflows/docker-publish.yml`.
diff --git a/docker/run/AGENTS.md b/docker/run/AGENTS.md
index bc07f5f5a..9077e5a90 100644
--- a/docker/run/AGENTS.md
+++ b/docker/run/AGENTS.md
@@ -22,9 +22,6 @@
- Do not bake secrets, local `.env` values, or user data into the image.
- Runtime startup must ensure `/a0/usr/uploads` exists before supervised services start.
- Runtime startup raises the soft open-file limit toward `A0_NOFILE_LIMIT` (default `65535`) before supervisord starts, bounded by the container hard limit.
-- Self-update user-data backups skip Time Travel shadow history under `usr/.time_travel/` and transient Desktop agent state.
-- Self-update waits up to 180 seconds for the updated or restored WebUI health check by default; `A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS` may override it.
-- Successful or already-current self-updates refresh an installed Codex CLI with npm on a best-effort basis; missing CLIs and registry failures must not block Agent Zero startup.
## Work Guidance
diff --git a/docker/run/fs/exe/self_update_manager.py b/docker/run/fs/exe/self_update_manager.py
index c0d008831..abf4e595f 100644
--- a/docker/run/fs/exe/self_update_manager.py
+++ b/docker/run/fs/exe/self_update_manager.py
@@ -36,7 +36,7 @@ DEFAULT_HEALTH_URL = os.environ.get(
"http://127.0.0.1:80/api/health",
)
DEFAULT_HEALTH_TIMEOUT_SECONDS = int(
- os.environ.get("A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS", "180")
+ os.environ.get("A0_SELF_UPDATE_HEALTH_TIMEOUT_SECONDS", "120")
)
DEFAULT_HEALTH_POLL_INTERVAL_SECONDS = float(
os.environ.get("A0_SELF_UPDATE_HEALTH_POLL_INTERVAL_SECONDS", "2")
@@ -405,11 +405,6 @@ def should_exclude_from_usr_backup(
logger: AttemptLogger,
) -> bool:
parts = relative_dir.parts
- if parts and parts[0] == ".time_travel":
- logger.log(
- f"Skipping Time Travel history during usr backup: {Path('usr') / relative_dir}"
- )
- return True
if (
len(parts) >= 6
and parts[0] == "plugins"
@@ -635,29 +630,6 @@ def clean_uv_cache(logger: AttemptLogger) -> None:
logger.log(f"uv cache clean skipped after error: {exc}")
-def refresh_codex_cli(logger: AttemptLogger) -> None:
- codex_path = shutil.which("codex")
- if not codex_path:
- logger.log("Codex CLI not installed, skipping Codex refresh.")
- return
-
- npm_path = shutil.which("npm")
- if not npm_path:
- logger.log("npm executable not found, skipping Codex refresh.")
- return
-
- logger.log("Refreshing the installed Codex CLI after self-update.")
- try:
- run_command(
- [npm_path, "install", "--global", "@openai/codex@latest"],
- cwd=None,
- logger=logger,
- error_message="Failed to refresh the installed Codex CLI.",
- )
- except Exception as exc:
- logger.log(f"Codex CLI refresh skipped after error: {exc}")
-
-
def has_local_rollback_changes(repo_dir: Path) -> bool:
status = git_output(repo_dir, "status", "--porcelain=v1", "--untracked-files=all")
return bool(status.strip())
@@ -1171,7 +1143,6 @@ def execute_pending_update(
logger=logger,
)
if healthy:
- refresh_codex_cli(logger)
record_result(
status="success",
message=f"Updated Agent Zero to branch {branch}, {resolved_target['target_description']}.",
@@ -1449,7 +1420,6 @@ def docker_run_ui() -> int:
logger.log(
"Requested tag already matches the installed version, skipping file replacement."
)
- refresh_codex_cli(logger)
record_result(
status="skipped",
message="Requested tag already matches the installed version.",
@@ -1493,11 +1463,8 @@ def main(argv: list[str] | None = None) -> int:
return docker_run_ui()
if args[0] == "trigger-update":
return trigger_update_command(args[1:])
- if args[0] == "refresh-codex":
- refresh_codex_cli(AttemptLogger(LOG_FILE))
- return 0
if args[0] in {"-h", "--help"}:
- print("Usage: self_update_manager.py [docker-run-ui | trigger-update ... | refresh-codex]")
+ print("Usage: self_update_manager.py [docker-run-ui | trigger-update ...]")
return 0
print(f"Unknown command: {args[0]}", file=sys.stderr)
return 1
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index c81fb55bd..3f3002b01 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -9,7 +9,6 @@
- `README.md`, `quickstart.md`, `guides/`, and `setup/` cover user-facing setup and workflows.
- `developer/` covers compact developer references and source handoffs.
-- `plans/` covers implementation plans, migration notes, and staged technical roadmaps.
- `res/` contains documentation images and other documentation assets.
## Local Contracts
diff --git a/docs/guides/a0-cli-connector.md b/docs/guides/a0-cli-connector.md
index f0812efc8..6dd4c2edc 100644
--- a/docs/guides/a0-cli-connector.md
+++ b/docs/guides/a0-cli-connector.md
@@ -132,22 +132,15 @@ machine.
- [ ] Keep A0 CLI connected to the Agent Zero chat.
- [ ] In Agent Zero Web UI, open Browser plugin settings and choose **Bring Your
Own Browser**.
-- [ ] If you want Agent Zero to use an already-open personal browser window,
- open that browser first.
-- [ ] In that browser, open its remote debugging page.
+- [ ] If you want Agent Zero to use an already-open personal Chrome window, open
+ that browser first.
+- [ ] In that browser, go to `chrome://inspect/#remote-debugging`.
- [ ] Enable **Allow remote debugging for this browser instance**.
-Remote debugging pages:
-
-| Browser | Page |
-| --- | --- |
-| Chrome, Edge, Brave, Vivaldi, Chromium | `chrome://inspect/#remote-debugging` |
-| Opera | `opera://inspect/#remote-debugging` |
-

When Agent Zero performs its first Browser action against that host browser,
-the browser asks for confirmation. Click **Allow** if you trust this Agent Zero
+Chrome asks for confirmation. Click **Allow** if you trust this Agent Zero
instance and A0 CLI connection.

@@ -156,30 +149,11 @@ A0 CLI does not take over the browser while it is only checking status. Browser
control starts when Agent Zero actually needs to use the browser.
> [!IMPORTANT]
-> Remote debugging gives the connected app full control of that browser session,
+> Remote debugging gives the connected app full control of that Chrome session,
> including access to saved data, cookies, site data, and navigation. Use it only
> with trusted Agent Zero instances and browser windows you intend the agent to
> control.
-The **Host browser** list in Browser settings comes from the connected local A0
-CLI, not from the Agent Zero Web UI server. It shows Automatic, currently
-advertised debug endpoints, and **Custom endpoint**. If a newly authorized
-browser does not appear, restart or reconnect A0 CLI.
-
-If the inspect checkbox is not enough for your browser build, launch it with an
-explicit remote debugging port and a separate profile:
-
-```bash
-opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
-```
-
-Then choose **Custom endpoint** in Browser settings, run `/browser ws://...` in
-A0 CLI, or pass the full DevTools websocket endpoint to A0 CLI:
-
-```bash
-export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."
-```
-
### Browser Profiles
```bash
diff --git a/docs/guides/browser.md b/docs/guides/browser.md
index 33d1b719f..ed406327a 100644
--- a/docs/guides/browser.md
+++ b/docs/guides/browser.md
@@ -137,8 +137,8 @@ development, Agent Zero can install it the first time it is needed.
## Bring Your Own Browser
-Bring Your Own Browser lets Agent Zero use Chrome, Edge, Brave, Opera, Vivaldi,
-or Chromium on your own computer through A0 CLI.
+Bring Your Own Browser lets Agent Zero use Chrome, Edge, or Chromium on your own
+computer through A0 CLI.
Use it when the page, login, or browser profile should stay on your machine.
@@ -146,35 +146,8 @@ Requirements:
- [ ] Keep A0 CLI connected to the Agent Zero chat.
- [ ] Choose **Bring Your Own Browser** in Browser settings.
-- [ ] Use a Chromium-family browser on the host: Chrome, Edge, Brave, Opera, Vivaldi, or Chromium.
-- [ ] For an already-open browser, open its remote debugging page and enable **Allow remote debugging for this browser instance**.
-
-Remote debugging pages:
-
-| Browser | Page |
-| --- | --- |
-| Chrome, Edge, Brave, Vivaldi, Chromium | `chrome://inspect/#remote-debugging` |
-| Opera | `opera://inspect/#remote-debugging` |
-
-The **Host browser** list shows Automatic, currently advertised debug endpoints,
-and **Custom endpoint**. If a browser does not appear after enabling remote
-debugging, restart or reconnect the local A0 CLI. Restarting only the Agent Zero
-Web UI server does not refresh the browser inventory; the list comes from the
-connected CLI.
-
-As a fallback, launch the browser with an explicit debugging port and profile
-directory:
-
-```bash
-opera --remote-debugging-port=9222 --user-data-dir="$HOME/.config/a0-opera-debug"
-```
-
-Then choose **Custom endpoint** in Browser settings, or pass the full DevTools
-websocket endpoint to the CLI:
-
-```bash
-export A0_HOST_BROWSER_REMOTE_DEBUGGING_ENDPOINTS="ws://127.0.0.1:9222/devtools/browser/..."
-```
+- [ ] Use Chrome, Edge, or Chromium on the host.
+- [ ] For personal Chrome remote debugging, open the host browser first, go to `chrome://inspect/#remote-debugging`, and enable **Allow remote debugging for this browser instance**.

diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md
index 21f8e979e..e41a7141a 100644
--- a/docs/guides/troubleshooting.md
+++ b/docs/guides/troubleshooting.md
@@ -41,7 +41,6 @@ PLAYWRIGHT_BROWSERS_PATH=tmp/playwright playwright install chromium
If **Bring Your Own Browser** mode fails:
- keep A0 CLI connected to the chat;
-- restart or reconnect A0 CLI after enabling remote debugging in a browser;
- run `/browser status` in A0 CLI;
- check that Browser settings still say **Bring Your Own Browser**;
- check **Page content access** if page text or screenshots are blocked.
diff --git a/extensions/python/_functions/AGENTS.md b/extensions/python/_functions/AGENTS.md
index db75fd4af..4a7331962 100644
--- a/extensions/python/_functions/AGENTS.md
+++ b/extensions/python/_functions/AGENTS.md
@@ -16,7 +16,6 @@
- Extension functions must match the implicit hook's supplied arguments.
- Preserve ordering prefixes where exception handling, watchdog registration, or cleanup depends on them.
- Hooks that mirror persisted AI responses into UI logs must reuse existing stream log items and avoid duplicating live response-tool logs.
-- Recovery-loop circuit breakers must stop at the General Settings limit and render their user-visible cost warning from a core framework prompt.
## Work Guidance
diff --git a/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py b/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py
deleted file mode 100644
index 2c44a910c..000000000
--- a/extensions/python/_functions/agent/Agent/hist_add_warning/end/_90_stop_unusable_response_loop.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from helpers.errors import HandledException
-from helpers.extension import Extension
-from helpers.settings import get_settings
-
-
-STATE_KEY = "_unusable_response_failures"
-
-
-class StopUnusableResponseLoop(Extension):
- def execute(self, data: dict | None = None, **kwargs):
- if not self.agent or not isinstance(data, dict):
- return
-
- call_kwargs = data.get("kwargs")
- message = call_kwargs.get("message") if isinstance(call_kwargs, dict) else None
- call_args = data.get("args")
- if message is None and isinstance(call_args, tuple) and len(call_args) > 1:
- message = call_args[1]
-
- if not isinstance(message, str):
- return
- if message not in {
- self.agent.read_prompt("fw.msg_misformat.md"),
- self.agent.read_prompt("fw.msg_repeat.md"),
- }:
- return
-
- loop_data = getattr(self.agent, "loop_data", None)
- state = getattr(loop_data, "params_persistent", None)
- iteration = getattr(loop_data, "iteration", None)
- if not isinstance(state, dict) or not isinstance(iteration, int):
- return
-
- previous = state.get(STATE_KEY, {})
- if not isinstance(previous, dict):
- previous = {}
- previous_iteration = previous.get("iteration")
- if previous_iteration == iteration:
- return
-
- count = (
- previous.get("count", 0) + 1
- if previous_iteration == iteration - 1
- else 1
- )
- state[STATE_KEY] = {"iteration": iteration, "count": count}
- limit = get_settings()["max_consecutive_unusable_responses"]
- if count < limit:
- return
-
- stop_message = self.agent.read_prompt(
- "fw.msg_unusable_response_limit.md", limit=limit
- )
- self.agent.context.log.log(type="warning", content=stop_message)
- data["exception"] = HandledException(stop_message)
diff --git a/extensions/python/message_loop_end/AGENTS.md b/extensions/python/message_loop_end/AGENTS.md
index 2200c4aa3..8927890a9 100644
--- a/extensions/python/message_loop_end/AGENTS.md
+++ b/extensions/python/message_loop_end/AGENTS.md
@@ -12,7 +12,6 @@
- Preserve history consistency before saving chats.
- Do not skip persistence for successful loops unless the hook contract explicitly permits it.
-- History compression that rewrites local history must clear the active Responses provider continuation while preserving stored response IDs for cleanup.
## Work Guidance
diff --git a/extensions/python/message_loop_end/_10_organize_history.py b/extensions/python/message_loop_end/_10_organize_history.py
index 0cc205bd6..d91c9ef80 100644
--- a/extensions/python/message_loop_end/_10_organize_history.py
+++ b/extensions/python/message_loop_end/_10_organize_history.py
@@ -1,18 +1,10 @@
from helpers.extension import Extension
from agent import LoopData
from helpers.defer import DeferredTask, THREAD_BACKGROUND
-from helpers.history import clear_responses_provider_state
DATA_NAME_TASK = "_organize_history_task"
-async def compress_history(agent) -> bool:
- compressed = bool(await agent.history.compress())
- if compressed:
- clear_responses_provider_state(agent)
- return compressed
-
-
class OrganizeHistory(Extension):
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
if not self.agent:
@@ -25,6 +17,6 @@ class OrganizeHistory(Extension):
# start task
task = DeferredTask(thread_name=THREAD_BACKGROUND)
- task.start_task(compress_history, self.agent)
+ task.start_task(self.agent.history.compress)
# set to agent to be able to wait for it
self.agent.set_data(DATA_NAME_TASK, task)
diff --git a/extensions/python/message_loop_prompts_after/AGENTS.md b/extensions/python/message_loop_prompts_after/AGENTS.md
index d18b1c5a1..ba8118a2f 100644
--- a/extensions/python/message_loop_prompts_after/AGENTS.md
+++ b/extensions/python/message_loop_prompts_after/AGENTS.md
@@ -2,21 +2,20 @@
## Purpose
-- Own prompt protocol, prompt extras, and history reattachment around primary message-loop prompt construction.
+- Own prompt protocol and extras appended around primary message-loop prompt construction.
## Ownership
-- Ordered Python files own current datetime, relevant-skill hints, loaded-skill history reattachment, agent info, parallel job status, and workdir extras injection.
+- Ordered Python files own current datetime, skill recall/load context, agent info, parallel job status, and workdir extras injection.
+- Active skill instructions belong in prompt protocol.
- Explicitly loaded skill bodies belong in tool-result history with metadata so they can survive persistence and be reattached after compaction.
- Explicitly loaded skill IDs are chat-wide context data, not agent-local state.
-- Skills must not write selected or loaded skill bodies into protocol or extras.
## Local Contracts
- Keep injected content bounded and clearly attributed.
- Preserve ordering where later prompt extras depend on earlier recall or load results.
- Do not expose secrets or private files from workdir extras.
-- Relevant-skill recall should search the raw user message when available, not the rendered history wrapper.
## Work Guidance
diff --git a/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py b/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
index a810cece4..c4b2a45e8 100644
--- a/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
+++ b/extensions/python/message_loop_prompts_after/_63_recall_relevant_skills.py
@@ -8,13 +8,9 @@ class RecallRelevantSkills(Extension):
if not self.agent or loop_data.iteration != 0:
return
- content = loop_data.user_message.content if loop_data.user_message else ""
- if isinstance(content, dict):
- user_instruction = str(content.get("user_message") or "").strip()
- else:
- user_instruction = (
- loop_data.user_message.output_text() if loop_data.user_message else ""
- ).strip()
+ user_instruction = (
+ loop_data.user_message.output_text() if loop_data.user_message else ""
+ ).strip()
if len(user_instruction) < 8:
return
diff --git a/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py b/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
index 6c604005a..d7effd627 100644
--- a/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
+++ b/extensions/python/message_loop_prompts_after/_65_include_loaded_skills.py
@@ -14,6 +14,9 @@ class IncludeLoadedSkills(Extension):
if not self.agent:
return
+ loop_data.protocol_persistent.pop("loaded_skills", None)
+ loop_data.extras_persistent.pop("loaded_skills", None)
+
skill_names = skills.get_loaded_skill_names(self.agent)
if not skill_names:
return
diff --git a/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py b/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
index 08966d2b1..b2c925fe8 100644
--- a/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
+++ b/extensions/python/message_loop_prompts_before/_90_organize_history_wait.py
@@ -1,9 +1,6 @@
from helpers.extension import Extension
from agent import LoopData
-from extensions.python.message_loop_end._10_organize_history import (
- DATA_NAME_TASK,
- compress_history,
-)
+from extensions.python.message_loop_end._10_organize_history import DATA_NAME_TASK
from helpers.defer import DeferredTask, THREAD_BACKGROUND
MAX_SYNC_COMPRESSION_PASSES = 64
@@ -36,7 +33,7 @@ class OrganizeHistoryWait(Extension):
else:
# no task was running, start and wait
self.agent.context.log.set_progress("Compressing history...")
- compressed = await compress_history(self.agent)
+ compressed = await self.agent.history.compress()
after_tokens = self.agent.history.get_tokens()
if not compressed or after_tokens >= before_tokens:
diff --git a/extensions/python/startup_migration/AGENTS.md b/extensions/python/startup_migration/AGENTS.md
index d3ffc39a0..fc75b8a8b 100644
--- a/extensions/python/startup_migration/AGENTS.md
+++ b/extensions/python/startup_migration/AGENTS.md
@@ -14,7 +14,6 @@
- Preserve user data and create backups or reversible paths when changing durable state.
- Keep long-running work bounded and observable.
- `_10_self_update_manager.py` may replace `/exe/self_update_manager.py` from the repository copy when the installed runtime updater is stale; it must validate required safety markers and keep a backup before replacement.
-- After synchronizing a stale self-update manager, `_10_self_update_manager.py` starts that manager's best-effort Codex CLI refresh in the background so the update that introduces the hook does not need a second restart.
## Work Guidance
diff --git a/extensions/python/startup_migration/_10_self_update_manager.py b/extensions/python/startup_migration/_10_self_update_manager.py
index fff24cc1c..8a188d6d0 100644
--- a/extensions/python/startup_migration/_10_self_update_manager.py
+++ b/extensions/python/startup_migration/_10_self_update_manager.py
@@ -3,8 +3,6 @@ from __future__ import annotations
import os
import shutil
import stat
-import subprocess
-import sys
from pathlib import Path
from typing import Any
@@ -27,8 +25,6 @@ REQUIRED_RUNTIME_MARKERS = (
"Skipping non-regular usr backup entry",
"def clean_transient_desktop_agent_state(",
"clean_transient_desktop_agent_state(REPO_DIR, logger)",
- "def refresh_codex_cli(",
- "refresh_codex_cli(logger)",
)
@@ -37,9 +33,6 @@ class SelfUpdateManagerRuntimeSync(Extension):
result = ensure_self_update_manager_runtime_current()
if result.get("updated"):
PrintStyle.info("Self-update manager runtime synchronized:", result["target"])
- warning = start_codex_cli_refresh(result["target"])
- if warning:
- PrintStyle.warning("Codex CLI refresh could not be started:", warning)
elif result.get("warning"):
PrintStyle.warning("Self-update manager runtime sync skipped:", result["warning"])
@@ -91,20 +84,6 @@ def ensure_self_update_manager_runtime_current(
}
-def start_codex_cli_refresh(manager_path: Path | str) -> str:
- try:
- subprocess.Popen(
- [sys.executable, str(manager_path), "refresh-codex"],
- stdin=subprocess.DEVNULL,
- stdout=subprocess.DEVNULL,
- stderr=subprocess.DEVNULL,
- start_new_session=True,
- )
- except OSError as exc:
- return str(exc)
- return ""
-
-
def _missing_required_markers(text: str) -> list[str]:
return [marker for marker in REQUIRED_RUNTIME_MARKERS if marker not in text]
diff --git a/extensions/python/system_prompt/AGENTS.md b/extensions/python/system_prompt/AGENTS.md
index c4aa1bfd8..34b90f78c 100644
--- a/extensions/python/system_prompt/AGENTS.md
+++ b/extensions/python/system_prompt/AGENTS.md
@@ -7,7 +7,7 @@
## Ownership
- Ordered Python files own main, tools, MCP, secrets, skills, and project prompt sections.
-- Active project instruction bodies and active-project AGENTS.md path-chain guidance are moved into prompt protocol; the system prompt keeps project metadata and stable project rules.
+- Active project instruction bodies are moved into prompt protocol; the system prompt keeps project metadata and stable project rules.
## Local Contracts
diff --git a/extensions/python/system_prompt/_14_project_prompt.py b/extensions/python/system_prompt/_14_project_prompt.py
index 4afa620cd..cd0363b4c 100644
--- a/extensions/python/system_prompt/_14_project_prompt.py
+++ b/extensions/python/system_prompt/_14_project_prompt.py
@@ -25,16 +25,9 @@ async def build_prompt(agent: Agent, loop_data: LoopData | None = None) -> str:
result = agent.read_prompt("agent.system.projects.main.md")
project_name = agent.context.get_data(projects.CONTEXT_DATA_KEY_PROJECT)
if loop_data:
- loop_data.protocol_persistent.pop("agents_md_instructions", None)
loop_data.protocol_persistent.pop("project_instructions", None)
if project_name:
project_vars = projects.build_system_prompt_vars(project_name)
- if loop_data and project_vars.get("include_agents_md", True):
- agents_md_protocol = projects.build_agents_md_protocol(project_name)
- if agents_md_protocol:
- loop_data.protocol_persistent["agents_md_instructions"] = (
- agents_md_protocol
- )
if loop_data and project_vars.get("project_instructions"):
loop_data.protocol_persistent["project_instructions"] = agent.read_prompt(
"agent.protocol.projects.instructions.md",
diff --git a/helpers/AGENTS.md b/helpers/AGENTS.md
index 39d24d71e..dc7584d32 100644
--- a/helpers/AGENTS.md
+++ b/helpers/AGENTS.md
@@ -15,7 +15,7 @@
- Preserve public helper APIs used by core code and plugins unless all callers, docs, and tests are updated.
- Use structured parsers and serializers for YAML, JSON, paths, and URLs instead of ad hoc string handling.
- Keep path handling constrained to intended roots for user files, uploads, downloads, projects, and workdirs.
-- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled, project instruction file content is injected with an explicit source path, and active-project AGENTS.md path-chain guidance is assembled into prompt protocol without duplicating the project root AGENTS.md.
+- Project metadata defaults must remain backwards-compatible; missing `include_agents_md` is treated as enabled and project instruction file content is injected with an explicit source path.
- Do not hardcode secrets, provider keys, local absolute paths, or environment-specific values.
- Use `RepairableException` for errors an agent may be able to fix.
- This directory is a file-documented DOX profile: every direct `*.py` helper module must have a same-directory `*.py.dox.md` file named by appending `.dox.md` to the full Python filename.
diff --git a/helpers/backup.py b/helpers/backup.py
index 70ad7b082..47e41c5f8 100644
--- a/helpers/backup.py
+++ b/helpers/backup.py
@@ -63,7 +63,6 @@ class BackupService:
return f"""# User data
# All persistent user data is now centralized in /usr for easier backup and restore
{agent_root}/usr/**
-!{agent_root}/usr/.time_travel/**
"""
def _get_agent_zero_version(self) -> str:
diff --git a/helpers/backup.py.dox.md b/helpers/backup.py.dox.md
index 5612447ec..e08f7a74e 100644
--- a/helpers/backup.py.dox.md
+++ b/helpers/backup.py.dox.md
@@ -26,7 +26,6 @@
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, settings/state persistence, secret handling.
- Imported dependency areas include: `datetime`, `helpers`, `helpers.localization`, `helpers.print_style`, `json`, `os`, `pathspec`, `platform`, `tempfile`, `typing`, `zipfile`.
- `test_patterns(..., max_files=None)` is the unlimited scan mode. UI preview and dry-run callers may pass bounded limits, but real backup creation and restore clean-before-restore must use unlimited matching so archives and cleanup are not silently truncated.
-- Default backup metadata includes persistent `/usr` data but excludes Time Travel shadow history under `usr/.time_travel/**`.
## Key Concepts
diff --git a/helpers/history.py b/helpers/history.py
index 1ff802d87..c579c501d 100644
--- a/helpers/history.py
+++ b/helpers/history.py
@@ -723,28 +723,6 @@ def output_text(messages: list[OutputMessage], ai_label="ai", human_label="human
return "\n".join(_stringify_output(o, ai_label, human_label) for o in messages)
-def clear_responses_provider_state(agent) -> None:
- key = getattr(agent, "DATA_NAME_RESPONSES_STATE", "responses_state")
- get_data = getattr(agent, "get_data", None)
- set_data = getattr(agent, "set_data", None)
- if not callable(get_data) or not callable(set_data):
- return
-
- state = get_data(key)
- if not isinstance(state, dict):
- return
-
- state = dict(state)
- removed = False
- for field in ("response_id", "previous_response_id"):
- if field in state:
- state.pop(field, None)
- removed = True
-
- if removed:
- set_data(key, state)
-
-
def _merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent:
if isinstance(a, str) and isinstance(b, str):
return a + "\n" + b
diff --git a/helpers/history.py.dox.md b/helpers/history.py.dox.md
index 53a7149e0..0e07d4a9e 100644
--- a/helpers/history.py.dox.md
+++ b/helpers/history.py.dox.md
@@ -65,7 +65,6 @@
- `group_messages_abab(messages: list[BaseMessage]) -> list[BaseMessage]`
- `output_langchain(messages: list[OutputMessage])`
- `output_text(messages: list[OutputMessage], ai_label=..., human_label=...)`
-- `clear_responses_provider_state(agent) -> None`
- `_merge_outputs(a: MessageContent, b: MessageContent) -> MessageContent`
- `_merge_properties(a: Dict[str, MessageContent], b: Dict[str, MessageContent]) -> Dict[str, MessageContent]`
- `_is_raw_message(obj: object) -> bool`
@@ -78,7 +77,6 @@
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
-- `clear_responses_provider_state(agent)` removes the active provider continuation IDs after local history rewrites while preserving stored response ID lists for later cleanup.
- Observed side-effect areas: filesystem writes, filesystem deletion, model calls, plugin state, settings/state persistence, secret handling.
- Imported dependency areas include: `abc`, `asyncio`, `collections`, `collections.abc`, `enum`, `helpers`, `json`, `langchain_core.messages`, `math`, `plugins._model_config.helpers.model_config`, `typing`, `uuid`.
diff --git a/helpers/mcp_handler.py b/helpers/mcp_handler.py
index e30c3916c..b1f888ec1 100644
--- a/helpers/mcp_handler.py
+++ b/helpers/mcp_handler.py
@@ -21,7 +21,6 @@ from contextlib import AsyncExitStack
from shutil import which
from datetime import timedelta
import json
-import shlex
import uuid
from helpers import errors
from helpers import settings
@@ -113,44 +112,6 @@ def _normalize_disabled_tools(value: Any) -> list[str]:
return [str(item).strip() for item in value if str(item).strip()]
-def _split_stdio_command(command: Any) -> tuple[str, list[str]]:
- text = str(command or "").strip()
- if not text:
- return "", []
- try:
- parts = shlex.split(text)
- except ValueError:
- return text, []
- if not parts:
- return "", []
- return parts[0], parts[1:]
-
-
-def _split_stdio_arg_fragment(arg: str) -> list[str]:
- try:
- parts = shlex.split(arg)
- except ValueError:
- return [arg]
- if len(parts) <= 1:
- return parts or []
- if parts[0].startswith("-") and "=" not in parts[0]:
- return parts
- if any(part.startswith("-") for part in parts[1:]):
- return parts
- return [arg]
-
-
-def _normalize_stdio_args(value: Any) -> list[str]:
- if not isinstance(value, list):
- return []
- args: list[str] = []
- for item in value:
- text = str(item).strip()
- if text:
- args.extend(_split_stdio_arg_fragment(text))
- return args
-
-
def initialize_mcp(mcp_servers_config: str):
if not MCPConfig.get_instance().is_initialized():
try:
@@ -688,15 +649,6 @@ class MCPServerLocal(BaseModel):
def update(self, config: dict[str, Any]) -> "MCPServerLocal":
with self.__lock:
- command = self.command
- command_args: list[str] = []
- args = list(self.args)
- if "command" in config:
- command, command_args = _split_stdio_command(config.get("command"))
- args = [*command_args, *args]
- if "args" in config:
- args = [*command_args, *_normalize_stdio_args(config.get("args"))]
-
for key, value in config.items():
if key in [
"name",
@@ -713,15 +665,11 @@ class MCPServerLocal(BaseModel):
"disabled_tools",
"scope",
]:
- if key in ["command", "args"]:
- continue
if key == "name":
value = normalize_name(value)
if key == "disabled_tools":
value = _normalize_disabled_tools(value)
setattr(self, key, value)
- self.command = command
- self.args = args
return self
async def initialize(self) -> "MCPServerLocal":
diff --git a/helpers/mcp_handler.py.dox.md b/helpers/mcp_handler.py.dox.md
index 1cab10612..4faff9fe1 100644
--- a/helpers/mcp_handler.py.dox.md
+++ b/helpers/mcp_handler.py.dox.md
@@ -66,9 +66,6 @@
- `_is_streaming_http_type(server_type: str) -> bool`: Check if the server type is a streaming HTTP variant.
- `_split_qualified_tool_name(tool_name: str) -> tuple[str, str]`: Split `server.tool` names while preserving dots inside MCP tool names.
- `_normalize_disabled_tools(value: Any) -> list[str]`: Normalize the optional per-server disabled tool list.
-- `_split_stdio_command(command: Any) -> tuple[str, list[str]]`: Split shell-style local MCP command lines into an executable plus leading arguments.
-- `_split_stdio_arg_fragment(arg: str) -> list[str]`: Split collapsed option/value argument fragments while preserving obvious single values with spaces.
-- `_normalize_stdio_args(value: Any) -> list[str]`: Normalize local MCP argument lists after manager/raw JSON parsing.
- `initialize_mcp(mcp_servers_config: str)`
- Notable constants/configuration names: `DEFAULT_MCP_SERVERS_CONFIG`, `MCP_MEDIA_TOKENS_ESTIMATE`, `MAX_MCP_RESOURCE_TEXT_CHARS`, `MCP_SESSION_CLEANUP_TIMEOUT_SECONDS`, `MCP_OPERATION_TIMEOUT_GRACE_SECONDS`, `T`.
@@ -84,13 +81,12 @@
- MCP tool names are qualified as `server_name.tool_name`; server names are normalized without dots, and the tool portion may contain dots.
- Servers may define `disabled_tools` as a list of MCP tool names. Disabled tools are omitted from agent-facing prompts, status counts, `has_tool`, and calls, while detail views can still retrieve them through `get_all_tools()` with a `disabled` flag so users can re-enable them.
- Server-specific `init_timeout` and `tool_timeout` override global MCP client timeout settings for list-tools and call-tool operations.
-- Local stdio server configs accept either strict MCP JSON (`command: "uvx", args: [...]`) or manager-style command lines (`command: "uvx package"`) and normalize them before spawning the process.
- MCP image and image-resource content is materialized to scoped artifact files and returned both as model-visible image attachments and as path metadata (`attachments`/`media_paths`) for downstream delivery.
- MCP config locks must not be held across awaited server initialization or tool-call operations. Slow or wedged MCP servers must not block status reads, prompt construction, unrelated MCP servers, or later tool calls through the shared config lock.
- MCP client session work runs inside disposable isolated `DeferredTask` workers with an outer timeout. Normal `AsyncExitStack` cleanup is also bounded; if cleanup or transport shutdown does not finish, the operation reports failure or warning while Agent Zero keeps control of the agent loop.
- Server status marks initialized server objects with cached initialization errors as disconnected, even if the config object exists.
- Observed side-effect areas: filesystem writes, network calls, WebSocket state, settings/state persistence, secret handling.
-- Imported dependency areas include: `abc`, `anyio.streams.memory`, `asyncio`, `contextlib`, `datetime`, `helpers`, `helpers.defer`, `helpers.log`, `helpers.print_style`, `helpers.tool`, `httpx`, `json`, `mcp`, `mcp.client.sse`, `mcp.client.stdio`, `mcp.client.streamable_http`, `mcp.shared.message`, `shlex`.
+- Imported dependency areas include: `abc`, `anyio.streams.memory`, `asyncio`, `contextlib`, `datetime`, `helpers`, `helpers.defer`, `helpers.log`, `helpers.print_style`, `helpers.tool`, `httpx`, `json`, `mcp`, `mcp.client.sse`, `mcp.client.stdio`, `mcp.client.streamable_http`, `mcp.shared.message`.
## Key Concepts
diff --git a/helpers/parallel_tools.py b/helpers/parallel_tools.py
index 0b9c9f72a..e9d830bef 100644
--- a/helpers/parallel_tools.py
+++ b/helpers/parallel_tools.py
@@ -487,36 +487,37 @@ async def _run_direct_tool_job(parent_context_id: str, job: ParallelJob) -> str:
worker_agent = worker_context.agent0
worker_agent.loop_data = LoopData()
- return await execute_tool_call(
- worker_agent,
- job.tool_name,
- job.tool_args,
- log_item=job.log_item,
- )
+ return await execute_tool_call(worker_agent, job.tool_name, job.tool_args)
finally:
if worker_context:
await _remove_context(worker_context.id)
-async def execute_tool_call(
- agent: "Agent",
- tool_name: str,
- tool_args: dict[str, Any],
- *,
- log_item: "LogItem | None" = None,
-) -> str:
+async def execute_tool_call(agent: "Agent", tool_name: str, tool_args: dict[str, Any]) -> str:
if tool_name == "parallel":
raise ValueError("`parallel` cannot be nested inside a parallel worker.")
- tool = _resolve_parallel_tool(agent, tool_name, tool_args, strict=True)
+ tool = None
+ try:
+ import helpers.mcp_handler as mcp_helper
+
+ tool = mcp_helper.MCPConfig.get_instance().get_tool(agent, tool_name)
+ except ImportError:
+ tool = None
+ except Exception as exc:
+ PrintStyle.warning(f"Failed to initialize MCP tool '{tool_name}' for parallel job: {exc}")
+
+ if not tool:
+ tool = agent.get_tool(
+ name=tool_name,
+ method=None,
+ args=tool_args,
+ message=json.dumps({"tool_name": tool_name, "tool_args": tool_args}),
+ loop_data=agent.loop_data,
+ )
if not tool:
raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
- original_get_log_object = None
- if log_item is not None:
- original_get_log_object = tool.get_log_object
- tool.get_log_object = lambda: log_item
-
agent.loop_data.current_tool = tool
try:
await agent.handle_intervention()
@@ -540,63 +541,9 @@ async def execute_tool_call(
await agent.handle_intervention()
return response.message
finally:
- if original_get_log_object is not None:
- tool.get_log_object = original_get_log_object
agent.loop_data.current_tool = None
-def _resolve_parallel_tool(
- agent: "Agent",
- tool_name: str,
- tool_args: dict[str, Any],
- *,
- strict: bool = False,
-):
- message = json.dumps({"tool_name": tool_name, "tool_args": tool_args})
-
- tool = None
- try:
- import helpers.mcp_handler as mcp_helper
-
- tool = mcp_helper.MCPConfig.get_instance().get_tool(agent, tool_name)
- except ImportError:
- tool = None
- except Exception as exc:
- if strict:
- raise
- PrintStyle.warning(f"Failed to initialize MCP tool '{tool_name}' for parallel job: {exc}")
-
- if not tool:
- get_tool = getattr(agent, "get_tool", None)
- if not callable(get_tool):
- if strict:
- raise ValueError(f"Tool '{tool_name}' not found or could not be initialized.")
- return None
- try:
- tool = get_tool(
- name=tool_name,
- method=None,
- args=tool_args,
- message=message,
- loop_data=getattr(agent, "loop_data", None),
- )
- except Exception as exc:
- if strict:
- raise
- PrintStyle.warning(f"Failed to initialize tool '{tool_name}' for parallel job: {exc}")
- tool = None
-
- if not tool:
- return None
-
- try:
- tool.args = dict(tool_args)
- except Exception:
- pass
-
- return tool
-
-
async def _cancel_job(
job: ParallelJob,
*,
@@ -653,17 +600,6 @@ def _log_parallel_child_started(agent: "Agent", job: ParallelJob) -> None:
)
return
- tool = _resolve_parallel_tool(agent, job.tool_name, job.tool_args)
- if tool is not None:
- try:
- job.log_item = tool.get_log_object()
- if job.log_item is not None:
- return
- except Exception as exc:
- PrintStyle.warning(
- f"Failed to derive parallel child log for {job.tool_name}: {exc}"
- )
-
heading = f"icon://construction {agent.agent_name}: Using tool '{job.tool_name}'"
job.log_item = agent.context.log.log(
type="tool",
diff --git a/helpers/parallel_tools.py.dox.md b/helpers/parallel_tools.py.dox.md
index d2456f2f2..39e65c4cc 100644
--- a/helpers/parallel_tools.py.dox.md
+++ b/helpers/parallel_tools.py.dox.md
@@ -31,8 +31,6 @@
- Direct tool background context cleanup removes both the in-memory context and any transient chat folder left on disk.
- Parent-visible child log items are created for each wrapped call so the WebUI can inspect concurrent children separately while the wrapper result remains model-history-only.
- Child tool logs mirror normal tool-call visible args; job ids remain available through wrapper results and prompt extras rather than visible process-step args.
-- Wrapped tool child logs use each tool's native `get_log_object()` output when available, preserving special log rendering (for example: `code_execution_tool` uses `code_exe`, `wait` uses `progress`, MCP tools use `mcp`, and regular tools use `tool`).
-- Direct parallel worker execution reuses the parent-visible child log item so tool `before_execution()` cannot create a second generic worker log or lose the native badge type.
- Job IDs are stable handles for later await, collect, or cancel operations.
- Prompt extras must stay bounded and expose only job IDs, tool names, status, and compact result/error summaries.
diff --git a/helpers/persist_chat.py b/helpers/persist_chat.py
index 47479bd35..199fa3ef5 100644
--- a/helpers/persist_chat.py
+++ b/helpers/persist_chat.py
@@ -1,15 +1,12 @@
-import json
-import os
-import tempfile
-import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any
-
+import uuid
from agent import Agent, AgentConfig, AgentContext, AgentContextType
from helpers import files, history
from helpers.litellm_transport import delete_stored_response_ids
from helpers.localization import Localization
+import json
from initialize import initialize_agent
from helpers.log import Log, LogItem
@@ -54,9 +51,10 @@ def save_tmp_chat(context: AgentContext):
return
path = _get_chat_file_path(context.id)
+ files.make_dirs(path)
data = _serialize_context(context)
js = _safe_json_serialize(data, ensure_ascii=False)
- _write_atomic(path, js)
+ files.write_file(path, js)
mark_chat_saved(context)
@@ -96,28 +94,6 @@ def _get_chat_file_path(ctxid: str):
return files.get_abs_path(CHATS_FOLDER, ctxid, CHAT_FILE_NAME)
-def _write_atomic(path: str, content: str) -> None:
- directory = os.path.dirname(path)
- os.makedirs(directory, exist_ok=True)
- fd, tmp_path = tempfile.mkstemp(
- prefix=f".{os.path.basename(path)}.", suffix=".tmp", dir=directory
- )
- try:
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
- handle.write(content.encode("utf-8", "replace").decode("utf-8"))
- handle.flush()
- os.fsync(handle.fileno())
- os.replace(tmp_path, path)
- directory_fd = os.open(directory, os.O_RDONLY)
- try:
- os.fsync(directory_fd)
- finally:
- os.close(directory_fd)
- finally:
- if os.path.exists(tmp_path):
- os.unlink(tmp_path)
-
-
def mark_chat_saved(context: AgentContext) -> None:
context.data[SAVED_CHAT_CONTEXT_DATA_KEY] = True
diff --git a/helpers/persist_chat.py.dox.md b/helpers/persist_chat.py.dox.md
index 09205e7d0..e75fe80d5 100644
--- a/helpers/persist_chat.py.dox.md
+++ b/helpers/persist_chat.py.dox.md
@@ -45,7 +45,6 @@
- Serialized chats store `agent_profile` both at the context level for the main chat and on each serialized agent so subordinate profiles survive server restart.
- Deserialization must rebuild each agent with its serialized profile when present, falling back to the context profile for older chat files.
- Chat loading skips directories that do not contain `chat.json`; malformed existing chat files still report load errors.
-- Chat saves write and fsync a same-directory temporary file, atomically replace `chat.json`, and fsync the directory so an interrupted save cannot truncate the previous chat.
- Contexts are marked with private `SAVED_CHAT_CONTEXT_DATA_KEY` only after a successful save or load from disk so snapshot code can detect deleted chat files without hiding fresh unsaved chats.
## Key Concepts
diff --git a/helpers/projects.py b/helpers/projects.py
index 41bbf9d44..f146d02a6 100644
--- a/helpers/projects.py
+++ b/helpers/projects.py
@@ -1,5 +1,5 @@
import os
-from typing import NotRequired, TypedDict, TYPE_CHECKING, cast
+from typing import Literal, NotRequired, TypedDict, TYPE_CHECKING, cast
from helpers import files, dirty_json, persist_chat, file_tree, extension
from helpers.print_style import PrintStyle
@@ -15,13 +15,7 @@ PROJECT_KNOWLEDGE_DIR = "knowledge"
PROJECT_SKILLS_DIR = "skills"
PROJECT_HEADER_FILE = "project.json"
PROJECT_MCP_SERVERS_FILE = "mcp_servers.json"
-PROJECT_AGENTS_MD_FILES = (
- "AGENTS.override.md",
- "AGENTS.Override.md",
- "AGENTS.md",
- "Agents.md",
- "agents.md",
-)
+PROJECT_AGENTS_MD_FILES = ("AGENTS.md", "Agents.md", "agents.md")
DEFAULT_MCP_SERVERS_CONFIG = '{\n "mcpServers": {}\n}'
CONTEXT_DATA_KEY_PROJECT = "project"
@@ -472,10 +466,9 @@ def deactivate_project_in_chats(name: str):
def build_system_prompt_vars(name: str):
project_data = load_basic_project_data(name)
main_instructions = project_data.get("instructions", "") or ""
- include_agents_md = project_data.get("include_agents_md", True)
instruction_files = get_project_instruction_files(
name,
- include_agents_md=include_agents_md,
+ include_agents_md=project_data.get("include_agents_md", True),
)
instruction_parts = [
main_instructions,
@@ -488,75 +481,11 @@ def build_system_prompt_vars(name: str):
"project_name": project_data.get("title", ""),
"project_description": project_data.get("description", ""),
"project_instructions": complete_instructions or "",
- "include_agents_md": include_agents_md,
"project_path": files.normalize_a0_path(get_project_folder(name)),
"project_git_url": project_data.get("git_url", ""),
}
-def get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]:
- root_real = os.path.realpath(files.fix_dev_path(root))
- target_real = os.path.realpath(files.fix_dev_path(target))
- if os.path.isfile(target_real):
- target_real = os.path.dirname(target_real)
-
- if files.is_in_dir(target_real, root_real):
- dirs = []
- cursor = target_real
- while True:
- dirs.append(cursor)
- if cursor == root_real:
- break
- parent = os.path.dirname(cursor)
- if parent == cursor:
- break
- cursor = parent
- dirs.reverse()
- else:
- dirs = [root_real]
-
- chain = []
- for dir_path in dirs:
- for filename in PROJECT_AGENTS_MD_FILES:
- matches = files.read_text_files_in_dir(dir_path, pattern=filename)
- if filename not in matches:
- continue
- chain.append((files.get_abs_path(dir_path, filename), matches[filename]))
- break
- return chain
-
-
-def build_agents_md_protocol(name: str, target: str | None = None) -> str:
- project_folder = get_project_folder(name)
- project_agents_md = get_project_agents_md_instruction_file(name)
- project_agents_md_path = (
- os.path.realpath(files.fix_dev_path(project_agents_md[0]))
- if project_agents_md
- else ""
- )
- entries = [
- (path, content)
- for path, content in get_agents_md_chain(
- files.get_abs_path(""),
- target or project_folder,
- )
- if os.path.realpath(path) != project_agents_md_path
- ]
- if not entries:
- return ""
-
- instructions = []
- for path, content in entries:
- instructions.append(
- f"### path: {files.normalize_a0_path(path)}\n\n{content.strip()}"
- )
- return files.read_prompt_file(
- "agent.protocol.projects.agents_md.md",
- _directories=["prompts"],
- agents_md_instructions="\n\n".join(instructions),
- ).strip()
-
-
def get_additional_instructions_files(name: str):
instructions_folder = files.get_abs_path(
get_project_folder(name), PROJECT_META_DIR, PROJECT_INSTRUCTIONS_DIR
@@ -593,8 +522,11 @@ def get_project_instruction_files(
def get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None:
project_folder = get_project_folder(name)
- for path, content in get_agents_md_chain(project_folder, project_folder):
- return (files.normalize_a0_path(path), content)
+ for filename in PROJECT_AGENTS_MD_FILES:
+ matches = files.read_text_files_in_dir(project_folder, pattern=filename)
+ if filename in matches:
+ path = files.get_abs_path(project_folder, filename)
+ return (files.normalize_a0_path(path), matches[filename])
return None
diff --git a/helpers/projects.py.dox.md b/helpers/projects.py.dox.md
index e1e1b592e..aae7514d0 100644
--- a/helpers/projects.py.dox.md
+++ b/helpers/projects.py.dox.md
@@ -47,8 +47,6 @@
- `reactivate_project_in_chats(name: str)`
- `deactivate_project_in_chats(name: str)`
- `build_system_prompt_vars(name: str)`
-- `get_agents_md_chain(root: str, target: str) -> list[tuple[str, str]]`
-- `build_agents_md_protocol(name: str, target: str | None=...) -> str`
- `get_additional_instructions_files(name: str)`
- `get_project_instruction_files(name: str, include_agents_md: bool=...) -> list[tuple[str, str]]`
- `get_project_agents_md_instruction_file(name: str) -> tuple[str, str] | None`
@@ -65,8 +63,6 @@
- Project extension data may add named top-level sections such as `llm`, but it must not overwrite core project fields owned by `EditProjectData`.
- Project extension save payloads exclude core project fields and transient inputs such as `git_token`; plugins needing core metadata should load it by project name.
- Project metadata setup creates and repairs `.a0proj/instructions`, `.a0proj/knowledge`, and `.a0proj/skills` so settings surfaces can open those folders consistently.
-- AGENTS.md discovery is a linear root-to-target chain walk with `AGENTS.override.md` precedence; sibling directories are not scanned.
-- Active-project AGENTS.md protocol guidance excludes the exact project root AGENTS.md because `build_system_prompt_vars(...)` already loads it into project instructions; prose for that protocol block lives in `prompts/agent.protocol.projects.agents_md.md`.
- Project MCP config uses the same JSON string shape as global MCP settings: an object with `mcpServers`.
- Project MCP load/save paths validate project names as simple folder basenames before touching `.a0proj/mcp_servers.json`.
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, plugin state, settings/state persistence, secret handling.
diff --git a/helpers/settings.py b/helpers/settings.py
index fcd66eef6..35b3f7beb 100644
--- a/helpers/settings.py
+++ b/helpers/settings.py
@@ -56,10 +56,8 @@ class Settings(TypedDict):
agent_profile: str
agent_knowledge_subdir: str
- max_consecutive_unusable_responses: int
timezone: str
time_format: str
- ui_control_visibility: dict[str, dict[str, bool]]
workdir_path: str
workdir_show: bool
@@ -165,12 +163,6 @@ API_KEY_PLACEHOLDER = "************"
TIMEZONE_AUTO = "auto"
TIME_FORMAT_12H = "12h"
TIME_FORMAT_24H = "24h"
-UI_CONTROL_VISIBILITY_DEFAULTS = {
- "projectSelector": {"mobile": True, "desktop": True},
- "time": {"mobile": False, "desktop": True},
- "connectionStatus": {"mobile": True, "desktop": True},
- "rightCanvasRail": {"mobile": True, "desktop": True},
-}
SETTINGS_FILE = files.get_abs_path("usr/settings.json")
_settings: Settings | None = None
@@ -217,22 +209,6 @@ def _normalize_time_format(value: Any, default: str = TIME_FORMAT_12H) -> str:
return default if default in {TIME_FORMAT_12H, TIME_FORMAT_24H} else TIME_FORMAT_12H
-def _normalize_ui_control_visibility(value: Any) -> dict[str, dict[str, bool]]:
- submitted = value if isinstance(value, dict) else {}
- normalized = {}
- for control, devices in UI_CONTROL_VISIBILITY_DEFAULTS.items():
- submitted_devices = submitted.get(control, {})
- if not isinstance(submitted_devices, dict):
- submitted_devices = {}
- normalized[control] = {
- device: submitted_devices.get(device)
- if isinstance(submitted_devices.get(device), bool)
- else default
- for device, default in devices.items()
- }
- return normalized
-
-
def _resolve_runtime_timezone(setting_value: str, browser_timezone: str | None = None) -> str:
if setting_value == TIMEZONE_AUTO:
candidate = str(browser_timezone or "").strip()
@@ -425,12 +401,8 @@ def normalize_settings(settings: Settings) -> Settings:
# mcp server token is set automatically
copy["mcp_server_token"] = create_auth_token()
- copy["max_consecutive_unusable_responses"] = max(
- 1, copy["max_consecutive_unusable_responses"]
- )
copy["timezone"] = _normalize_timezone_setting(copy.get("timezone"), default["timezone"])
copy["time_format"] = _normalize_time_format(copy.get("time_format"), default["time_format"])
- copy["ui_control_visibility"] = _normalize_ui_control_visibility(copy.get("ui_control_visibility"))
return copy
@@ -526,14 +498,8 @@ def get_default_settings() -> Settings:
root_password="",
agent_profile=get_default_value("agent_profile", "agent0"),
agent_knowledge_subdir=get_default_value("agent_knowledge_subdir", "custom"),
- max_consecutive_unusable_responses=get_default_value(
- "max_consecutive_unusable_responses", 2
- ),
timezone=_normalize_timezone_setting(get_default_value("timezone", TIMEZONE_AUTO)),
time_format=_normalize_time_format(get_default_value("time_format", TIME_FORMAT_12H)),
- ui_control_visibility=_normalize_ui_control_visibility(
- get_default_value("ui_control_visibility", UI_CONTROL_VISIBILITY_DEFAULTS)
- ),
workdir_path=get_default_value("workdir_path", files.get_abs_path_dockerized("usr/workdir")),
workdir_show=get_default_value("workdir_show", True),
workdir_max_depth=get_default_value("workdir_max_depth", 5),
diff --git a/helpers/settings.py.dox.md b/helpers/settings.py.dox.md
index d4bdb974c..d5b61ce92 100644
--- a/helpers/settings.py.dox.md
+++ b/helpers/settings.py.dox.md
@@ -25,7 +25,6 @@
- `_is_valid_timezone(value: str) -> bool`
- `_normalize_timezone_setting(value: Any, default: str=...) -> str`
- `_normalize_time_format(value: Any, default: str=...) -> str`
-- `_normalize_ui_control_visibility(value: Any) -> dict[str, dict[str, bool]]`
- `_resolve_runtime_timezone(setting_value: str, browser_timezone: str | None=...) -> str`
- `_timezone_options() -> list[FieldOption]`
- `convert_out(settings: Settings) -> SettingsOutput`
@@ -51,7 +50,7 @@
- `_dict_to_env(data_dict)`
- `set_root_password(password: str)`
- `get_runtime_config(set: Settings)`
-- Notable constants/configuration names: `T`, `PASSWORD_PLACEHOLDER`, `API_KEY_PLACEHOLDER`, `TIMEZONE_AUTO`, `TIME_FORMAT_12H`, `TIME_FORMAT_24H`, `UI_CONTROL_VISIBILITY_DEFAULTS`, `SETTINGS_FILE`.
+- Notable constants/configuration names: `T`, `PASSWORD_PLACEHOLDER`, `API_KEY_PLACEHOLDER`, `TIMEZONE_AUTO`, `TIME_FORMAT_12H`, `TIME_FORMAT_24H`, `SETTINGS_FILE`.
## Runtime Contracts
@@ -65,8 +64,6 @@
- Important called helpers/classes observed in the source: `TypeVar`, `files.get_abs_path`, `dotenv.get_dotenv_value`, `opts.insert`, `str.strip`, `_is_valid_timezone`, `str.strip.lower`, `_normalize_timezone_setting`, `SettingsOutput`, `get_default_settings`, `_ensure_option_present`, `_resolve_runtime_timezone`, `get_default_secrets_manager`, `get_settings`, `normalize_settings`, `_load_sensitive_settings`, `settings.copy`, `_write_settings_file`, `reload_settings`, `set_settings`, `initialize_agent`.
- Applying settings refreshes active context configs while preserving each subordinate agent's own profile.
- Applying settings starts a deferred `MCPConfig.update(...)` with the current `mcp_servers` string when global MCP server settings change.
-- `max_consecutive_unusable_responses` defaults to `2` and controls the cost circuit breaker for malformed or repeated main-model outputs.
-- `ui_control_visibility` stores validated mobile and desktop visibility flags for the project selector, clock, connection status, and right canvas rail; missing or malformed values fall back per device.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
diff --git a/helpers/skills.py b/helpers/skills.py
index 1eeaa8504..74488da9d 100644
--- a/helpers/skills.py
+++ b/helpers/skills.py
@@ -543,15 +543,11 @@ def search_skills(
if not q:
return []
- raw_terms = re.findall(r"[a-z0-9][a-z0-9_-]*", q)
+ raw_terms = [t for t in re.split(r"\s+", q) if t]
terms = [
t for t in raw_terms
- if len(t) >= 4 or any(ch.isdigit() for ch in t)
- ]
- long_terms = [
- t for t in raw_terms
- if len(t) >= 6 or any(ch.isdigit() for ch in t)
- ]
+ if len(t) >= 3 or any(ch.isdigit() for ch in t)
+ ] or raw_terms
candidates = list_skills(agent, include_hidden=include_hidden)
scored: List[Tuple[int, Skill]] = []
@@ -572,13 +568,14 @@ def search_skills(
score += 4
if any(q in tag for tag in tags):
score += 3
- if any(q in trigger or trigger in q for trigger in triggers):
+ if any(q in trigger for trigger in triggers):
score += 8
for term in terms:
if term in name:
score += 3
- for term in long_terms:
+ if term in desc:
+ score += 2
if any(term in tag for tag in tags):
score += 1
if any(term in trigger for trigger in triggers):
@@ -1135,6 +1132,7 @@ def hide_chat_skill(agent: Agent, entry: Any) -> list[ActiveSkillEntry]:
CONTEXT_DATA_NAME_CHAT_VISIBLE_SKILLS,
visible_entries,
)
+ unload_agent_skill(agent, normalized)
return get_hidden_skills(agent)
@@ -1185,7 +1183,8 @@ def clear_chat_skill_overrides(agent: Agent) -> list[ActiveSkillEntry]:
def build_active_skills_prompt(agent: Agent | None) -> str:
- return ""
+ items = _resolve_active_skill_entries(agent, get_active_skills(agent))
+ return "\n\n".join(item["content"] for item in items if item.get("content")).strip()
def _format_skill_prompt(skill: Skill) -> str:
diff --git a/helpers/skills.py.dox.md b/helpers/skills.py.dox.md
index 902caf3fc..d269bb205 100644
--- a/helpers/skills.py.dox.md
+++ b/helpers/skills.py.dox.md
@@ -56,9 +56,6 @@
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
- Loaded skill names are chat-wide context data under `CONTEXT_DATA_NAME_LOADED_SKILLS`; legacy agent-local `loaded_skills` lists are migrated into context data and cleared when read.
-- Loaded skill bodies live in chat history; hiding a skill changes catalog visibility but does not remove the loaded-skill ledger.
-- `build_active_skills_prompt()` returns empty because selected skills are loaded through history, not prompt protocol.
-- `search_skills()` normalizes query words, scores normal terms against skill names, and scores only long terms against tags/triggers; descriptions match only full query phrases so generic prose does not produce irrelevant suggestions.
- Invalid `SKILL.md` frontmatter emits a once-per-path scan warning with the skipped skill path/name and a line number when the parser can identify one directly.
- Observed side-effect areas: filesystem reads, filesystem deletion, plugin state, settings/state persistence, context data, secret handling.
- Imported dependency areas include: `__future__`, `dataclasses`, `helpers`, `os`, `pathlib`, `re`, `typing`.
diff --git a/helpers/ui_server.py b/helpers/ui_server.py
index fdea68b01..cbf14a054 100644
--- a/helpers/ui_server.py
+++ b/helpers/ui_server.py
@@ -1,7 +1,6 @@
from dataclasses import dataclass, field
from datetime import timedelta
import asyncio
-import json
import logging
import os
import secrets
@@ -263,13 +262,6 @@ class UiRouteHandlers:
user_time_format_setting = str(settings_helper.get_settings().get("time_format", "12h"))
except Exception:
user_time_format_setting = "12h"
- try:
- user_ui_control_visibility = json.dumps(
- settings_helper.get_settings()["ui_control_visibility"],
- separators=(",", ":"),
- )
- except Exception:
- user_ui_control_visibility = json.dumps(settings_helper.UI_CONTROL_VISIBILITY_DEFAULTS)
index = files.read_file("webui/index.html")
return files.replace_placeholders_text(
@@ -281,7 +273,6 @@ class UiRouteHandlers:
logged_in=("true" if login.get_credentials_hash() else "false"),
user_timezone_setting=user_timezone_setting,
user_time_format_setting=user_time_format_setting,
- user_ui_control_visibility=user_ui_control_visibility,
)
@requires_auth
diff --git a/helpers/ui_server.py.dox.md b/helpers/ui_server.py.dox.md
index ae430843f..4c993c84d 100644
--- a/helpers/ui_server.py.dox.md
+++ b/helpers/ui_server.py.dox.md
@@ -41,7 +41,6 @@
## Key Concepts
- Important called helpers/classes observed in the source: `logging.getLogger.setLevel`, `Localization.get.apply_process_timezone`, `_positive_int_env`, `field`, `Flask`, `threading.RLock`, `socketio.AsyncServer`, `WsManager`, `set_shared_ws_manager`, `cls`, `server_runtime.refresh_runtime_settings`, `settings_helper.get_settings`, `settings_helper.set_runtime_settings_snapshot`, `self.ws_manager.set_server_restart_broadcast`, `UiRouteHandlers`, `self.webapp.add_url_rule`, `register_api_route`, `register_ws_namespace`, `files.read_file`, `render_template_string`, `session.pop`.
-- `serve_index()` bootstraps the normalized UI control visibility map alongside timezone and time-format preferences so controls render correctly before Settings is opened.
- Keep request/response, tool, or helper semantics documented here at the same time as source changes.
## Work Guidance
diff --git a/plugins/AGENTS.md b/plugins/AGENTS.md
index 0d42f9124..768a203b0 100644
--- a/plugins/AGENTS.md
+++ b/plugins/AGENTS.md
@@ -70,7 +70,6 @@ Direct child DOX files:
| [_browser/AGENTS.md](_browser/AGENTS.md) | Playwright browser tool, helpers, viewer, and browser panel UI. |
| [_chat_branching/AGENTS.md](_chat_branching/AGENTS.md) | Chat branching from an existing message. |
| [_chat_compaction/AGENTS.md](_chat_compaction/AGENTS.md) | Full-chat compaction into a summary message. |
-| [_commands/AGENTS.md](_commands/AGENTS.md) | Built-in slash command manager, command file discovery, and chat composer slash picker. |
| [_code_execution/AGENTS.md](_code_execution/AGENTS.md) | Terminal, Python, and Node.js execution tools and shell runtimes. |
| [_desktop/AGENTS.md](_desktop/AGENTS.md) | Linux desktop runtime, sessions, and desktop surface. |
| [_discovery/AGENTS.md](_discovery/AGENTS.md) | Welcome-screen plugin discovery cards and promotions. |
@@ -78,7 +77,6 @@ Direct child DOX files:
| [_editor/AGENTS.md](_editor/AGENTS.md) | Native Markdown editor surface and sessions. |
| [_email_integration/AGENTS.md](_email_integration/AGENTS.md) | IMAP/Exchange polling and SMTP reply integration. |
| [_error_retry/AGENTS.md](_error_retry/AGENTS.md) | Critical exception retry lifecycle hooks. |
-| [_goal/AGENTS.md](_goal/AGENTS.md) | Built-in chat goal strip, `/goal` slash command, and agent-facing goal tools. |
| [_infection_check/AGENTS.md](_infection_check/AGENTS.md) | Prompt-injection safety analysis before tool execution. |
| [_kokoro_tts/AGENTS.md](_kokoro_tts/AGENTS.md) | Kokoro text-to-speech integration. |
| [_memory/AGENTS.md](_memory/AGENTS.md) | Optional persistent recall plugin, knowledge import, tools, and dashboard; do not assume it is enabled outside this plugin. |
@@ -86,7 +84,6 @@ Direct child DOX files:
| [_oauth/AGENTS.md](_oauth/AGENTS.md) | OAuth-backed model-provider connections and local proxy routes. |
| [_office/AGENTS.md](_office/AGENTS.md) | LibreOffice office artifacts and office canvas sessions. |
| [_onboarding/AGENTS.md](_onboarding/AGENTS.md) | First-time model onboarding wizard. |
-| [_orchestrator/AGENTS.md](_orchestrator/AGENTS.md) | External terminal coding-agent orchestration skill, adapter status, and settings UI. |
| [_plugin_installer/AGENTS.md](_plugin_installer/AGENTS.md) | Plugin install and update flows from ZIP, Git, and Plugin Index. |
| [_plugin_scan/AGENTS.md](_plugin_scan/AGENTS.md) | LLM-guided security scanner for third-party plugins. |
| [_plugin_validator/AGENTS.md](_plugin_validator/AGENTS.md) | Plugin manifest, structure, convention, and security validator. |
diff --git a/plugins/_a0_connector/AGENTS.md b/plugins/_a0_connector/AGENTS.md
index b81567a24..e013c73cf 100644
--- a/plugins/_a0_connector/AGENTS.md
+++ b/plugins/_a0_connector/AGENTS.md
@@ -24,7 +24,6 @@
- File operation results may arrive as chunked JSON/base64
`connector_file_op_result` frames; resolve the pending file operation only
after all chunks for the `op_id` are assembled.
-- Host browser status metadata may advertise `available_browsers` entries with browser ids, labels, CDP endpoints, status, and enabled state; keep older CLI payloads without those fields compatible.
## Work Guidance
diff --git a/plugins/_a0_connector/api/v1/browser_runtime.py b/plugins/_a0_connector/api/v1/browser_runtime.py
index ae9b63829..72467e954 100644
--- a/plugins/_a0_connector/api/v1/browser_runtime.py
+++ b/plugins/_a0_connector/api/v1/browser_runtime.py
@@ -24,11 +24,6 @@ def _normalize_requested_backend(value: object) -> str:
return ""
-def _normalize_host_browser_selection(value: object) -> str:
- raw = _string(value).lower().replace(" ", "_")
- return "".join(ch for ch in raw if ch.isalnum() or ch in {"_", "-", ":", ".", "/"})[:200]
-
-
def _normalize_profile_mode(value: object) -> str:
normalized = _string(value).lower().replace("-", "_").replace(" ", "_")
if normalized in {"agent", "clean", "clean_agent", "a0", "dedicated"}:
@@ -73,10 +68,6 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
mimetype="application/json",
)
settings["runtime_backend"] = runtime_backend
- if "host_browser_selection" in input or "browser_selection" in input:
- settings["host_browser_selection"] = _normalize_host_browser_selection(
- input.get("host_browser_selection", input.get("browser_selection"))
- )
if "host_browser_profile_mode" in input or "profile_mode" in input:
profile_mode = _normalize_profile_mode(
input.get("host_browser_profile_mode", input.get("profile_mode"))
@@ -95,13 +86,11 @@ class BrowserRuntime(connector_base.ProtectedConnectorApiHandler):
runtime_backend = settings.get("runtime_backend") or "container"
profile_mode = _normalize_profile_mode(settings.get("host_browser_profile_mode")) or "existing"
- browser_selection = _normalize_host_browser_selection(settings.get("host_browser_selection"))
return {
"ok": True,
"runtime_backend": runtime_backend,
"host_browser_profile_mode": profile_mode,
- "host_browser_selection": browser_selection,
"label": _runtime_label(runtime_backend),
"project_name": project_name,
"agent_profile": "",
diff --git a/plugins/_a0_connector/helpers/ws_runtime.py b/plugins/_a0_connector/helpers/ws_runtime.py
index e35948359..2e6b7034f 100644
--- a/plugins/_a0_connector/helpers/ws_runtime.py
+++ b/plugins/_a0_connector/helpers/ws_runtime.py
@@ -80,9 +80,6 @@ class HostBrowserMetadata:
profile_label: str
profile_path: str
cdp_endpoint: str
- browser_id: str
- browser_label: str
- available_browsers: tuple[dict[str, Any], ...]
content_helper_sha256: str
features: tuple[str, ...]
support_reason: str
@@ -419,9 +416,6 @@ def store_sid_host_browser_metadata(sid: str, payload: dict[str, Any]) -> HostBr
profile_label=str(payload.get("profile_label", "") or "").strip(),
profile_path=str(payload.get("profile_path", "") or "").strip(),
cdp_endpoint=str(payload.get("cdp_endpoint", "") or "").strip(),
- browser_id=str(payload.get("browser_id", payload.get("browser_selection", "")) or "").strip(),
- browser_label=str(payload.get("browser_label", "") or "").strip(),
- available_browsers=_normalize_available_host_browsers(payload.get("available_browsers")),
content_helper_sha256=str(payload.get("content_helper_sha256", "") or "").strip().lower(),
features=features,
support_reason=support_reason,
@@ -451,32 +445,6 @@ def _host_browser_can_prepare(
)
-def _normalize_available_host_browsers(value: Any) -> tuple[dict[str, Any], ...]:
- if not isinstance(value, (list, tuple)):
- return ()
- browsers: list[dict[str, Any]] = []
- for item in value:
- if not isinstance(item, dict):
- continue
- browser_id = str(item.get("id", item.get("browser_id", item.get("selection", ""))) or "").strip()
- family = str(item.get("family", item.get("browser_family", "")) or "").strip()
- label = str(item.get("label", item.get("name", "")) or "").strip()
- cdp_endpoint = str(item.get("cdp_endpoint", "") or "").strip()
- status = str(item.get("status", "") or "").strip()
- enabled = bool(item.get("enabled", True))
- if not any((browser_id, family, label, cdp_endpoint)):
- continue
- browsers.append({
- "id": browser_id or family or cdp_endpoint,
- "family": family,
- "label": label or family or browser_id or cdp_endpoint,
- "cdp_endpoint": cdp_endpoint,
- "status": status,
- "enabled": enabled,
- })
- return tuple(browsers)
-
-
def clear_sid_host_browser_metadata(sid: str) -> None:
with _state_lock:
_sid_host_browser_metadata.pop(sid, None)
@@ -496,9 +464,6 @@ def host_browser_metadata_for_sid(sid: str) -> dict[str, Any] | None:
"profile_label": metadata.profile_label,
"profile_path": metadata.profile_path,
"cdp_endpoint": metadata.cdp_endpoint,
- "browser_id": metadata.browser_id,
- "browser_label": metadata.browser_label,
- "available_browsers": copy.deepcopy(list(metadata.available_browsers)),
"content_helper_sha256": metadata.content_helper_sha256,
"features": list(metadata.features),
"support_reason": metadata.support_reason,
@@ -564,10 +529,6 @@ def all_host_browser_metadata() -> list[dict[str, Any]]:
"browser_family": metadata.browser_family,
"profile_label": metadata.profile_label,
"profile_path": metadata.profile_path,
- "cdp_endpoint": metadata.cdp_endpoint,
- "browser_id": metadata.browser_id,
- "browser_label": metadata.browser_label,
- "available_browsers": copy.deepcopy(list(metadata.available_browsers)),
"content_helper_sha256": metadata.content_helper_sha256,
"features": list(metadata.features),
"support_reason": metadata.support_reason,
diff --git a/plugins/_browser/AGENTS.md b/plugins/_browser/AGENTS.md
index a0e570a9d..70211d3a0 100644
--- a/plugins/_browser/AGENTS.md
+++ b/plugins/_browser/AGENTS.md
@@ -19,15 +19,7 @@
- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility across regular host browsers and Electron WebContentsView embedding.
- Keep the WebUI Browser inside its own modal/canvas affordance; do not replace it with page-level navigation.
- Default the visible WebUI Browser to live CDP screencast for responsiveness. Keep lightweight CDP/DOM state snapshots as the fallback transport.
-- Paint live screencast frames through the Browser panel canvas/ImageBitmap path when available; keep the `