From 4ca1f7fc0f5b7b4906064297d6acc0a2e39f1b23 Mon Sep 17 00:00:00 2001 From: "Alain@Runwaize" Date: Mon, 16 Mar 2026 16:18:02 +0200 Subject: [PATCH 1/6] feat: unify --local mode to run user agents alongside Hello World (#10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(server): add local-mode handling and hello-world injectionAdd for unified "--local" behavior in Server and tooling. - import Path in src/supervaizer/server.py support filesystem checks- detect SUPERVA_LOCAL_MODE in Server.__init__ and: - force supervisor_account=None in local mode (log if overridden) - api_key to "local-dev" when not explicitly set or via env - enable admin_interface and a2a_end automatically - prepend Hello World agent (via get_default_local_agent) unless SUPERVAIZER_DISABLE_HELLO_WORLD=true or an agent with the slug exists - ensure injection happens before route setup UI and endpoints include the injected agentAdd gh-release target to justfile for creating GitHub releases- new gh-release recipe reads, creates a v release, notes commits since previous tag, publishes to the supervaize/supervaizer repoAdd design doc describing local mode unification- docs/superpowers/specs/2026-03-16-manage-hello-world-design: - explains goals, design decisions, CLI/server/local_server changes, and cases for combining user agents with the World agentMotivation: - Unify local and normal runtime paths so users see their agents in --local mode preserving an easy Hello World fallback and avoiding Studio registration/credentials in local dev. The-release target automates creating GitHub releases from the repo version. * feat: unify --local mode to run user agents alongside Hello World - Server.__init__ detects SUPERVAIZER_LOCAL_MODE env var: - Forces supervisor_account=None (skips Studio registration) - Injects Hello World agent unless SUPERVAIZER_DISABLE_HELLO_WORLD=true - Defaults api_key to 'local-dev' - Deploys agent routes in local mode - CLI --local sets env var and falls through to normal subprocess path - Falls back to local_server.py when no supervaizer_control.py exists - Simplified local_server.py: removed create_local_server(), added __main__ - Added 8 server tests + 2 CLI tests for local mode behavior * ✨feat(server): default api_key to local-dev in local modeUpdate tests and docs to reflect that when SUPERVAIZER_LOCAL_MODE truethe server defaults its api_key to "local" even if SUPERVAIZER_API_KEYis. Adjust test wording and expectations to reference theSUPERVAIZER_API_KEY env var rather than a "explicit api_key". Expand and reformat design docs and implementation for managing theHello World agent local mode: CLI paths, set SUPAIZER_LOCAL_MODE --local, force supervisor_account=None in local mode, inject HelloWorld unless disabled, and enable admin/a2a endpoints Clar edge-casematrix and add a full implementation plan document. Why- Make local mode behavior explicit and consistent across, tests and docs. - Provide a clear plan and spec for injecting and managing the Hello agent and related changes. * fix: add missing field declarations to CaseNode model CaseNode was missing Pydantic field definitions for name, type, factory, description, and can_be_confirmed. This caused AttributeError on all 9 test_case.py tests. * ✨ feat(readme): add "local mode" docs and env varsIntroduce a Local Mode section and related CLI/help updates to letdevelopers run the server entirely offline local testing anddevelopment. - "6. Local mode" ( shift Optional parameters to7) to README, documenting `supervaizer start --local`, behavior supervaizer_control.py is absent, and how the built-in World agent is managed. - Document automatic environment changes for local mode: SUPERVAIZER_LOCAL_MODE=true - default API key set to `local-dev` (overridable via SUPERVAIZER_API_KEY) - option to disable Hello World viaERVAIZER_DISABLE_HELO_WORLD- Update CLI examples docs/CLI to `--local` usage and corresponding env var notes. - AddERVAIZER_MODE and SUPERVA_DISABLELLO_WORLD the env var in to make configurationable. This makes local development simpler and reduces friction for testingagents without relying on Studio/ registration. * ✨ feat: improve human_answer handling and server info storage- Refactor work human_answer invocation to more robustly check for agent.methods and the presence of human_answer definition before attempting to execute it. avoids AttributeError and clar conditional logic while the existing behavior of running human_answer method in an executor and handling exceptions logged and error. - Update server info creation to use a fixed singleton id ("server_instance") when saving to storage subsequent retrievals can reliably the stored ServerInfo entry. - Add Unit Tests Results section to CHANGELOG with test counts and time. --- README.md | 20 +- docs/CHANGELOG.md | 17 +- docs/CLI.md | 13 + .../plans/2026-03-16-manage-hello-world.md | 481 ++++++++++++++++++ .../2026-03-16-manage-hello-world-design.md | 63 +++ justfile | 17 + src/supervaizer/admin/routes.py | 1 - src/supervaizer/admin/workbench_routes.py | 46 +- src/supervaizer/case.py | 12 +- src/supervaizer/cli.py | 45 +- src/supervaizer/examples/local_server.py | 45 +- src/supervaizer/server.py | 42 +- tests/test_cli.py | 26 + tests/test_server.py | 146 ++++++ 14 files changed, 883 insertions(+), 91 deletions(-) create mode 100644 docs/superpowers/plans/2026-03-16-manage-hello-world.md create mode 100644 docs/superpowers/specs/2026-03-16-manage-hello-world-design.md diff --git a/README.md b/README.md index 000fc5d..8c34da9 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ A Python toolkit for building, managing, and connecting AI agents with full [Age - [3. Scaffold the controller](#3-scaffold-the-controller) - [(Optional) 4. Configure your Supervaize account \& environment](#optional-4-configure-your-supervaize-account--environment) - [5. Start the server πŸš€](#5-start-the-server-) - - [6. Optional parameters](#6-optional-parameters) + - [6. Local mode](#6-local-mode) + - [7. Optional parameters](#7-optional-parameters) - [What's next?](#whats-next) - [Features](#features) - [Protocol Support](#protocol-support) @@ -117,7 +118,22 @@ Once the server is running, you'll have: - **A2A discovery**: `/.well-known/agents.json` - **ACP discovery**: `/agents` -### 6. Optional parameters +### 6. Local mode + +Run the server locally without connecting to Studio: + +```bash +supervaizer start --local +``` + +This starts the server with your agents from `supervaizer_control.py` alongside a built-in Hello World agent. If no `supervaizer_control.py` exists, only the Hello World agent is loaded. + +- **No Studio registration** β€” the server runs fully offline +- **`SUPERVAIZER_LOCAL_MODE=true`** is set automatically +- **API key** defaults to `local-dev` (override with `SUPERVAIZER_API_KEY`) +- **Disable Hello World** by setting `SUPERVAIZER_DISABLE_HELLO_WORLD=true` + +### 7. Optional parameters Configure retry behavior for HTTP requests to the Supervaize API: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 96c28f4..ce98500 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,7 +19,7 @@ All notable changes to this project will be documented in this file. ## Unreleased -### Added +### v0.10.27 - **Agent Workbench** β€” Full-featured testing interface for agents directly from the admin panel. Four-zone layout with agent parameters, job control, execution monitor, and live console log. Supports starting/stopping jobs, real-time case and step tracking via HTMX polling, and Human-in-the-Loop (HITL) form rendering and submission. Job history panel lists all past executions with status badges. - Backend: `workbench_routes.py` with 8 FastAPI endpoints (page, start, stop, status, monitor, console, HITL answer, job history) @@ -44,6 +44,15 @@ All notable changes to this project will be documented in this file. - **CI** – Python package workflow: dependency install now uses `uv sync --extra dev --extra deploy` (adds deploy extras for CI). +### Unit Tests Results + +| Status | Count | +| ---------- | ----- | +| βœ… Passed | 433 | +| πŸ€” Skipped | 0 | +| πŸ”΄ Failed | 0 | +| ⏱️ in | 50s | + ## v0.10.19 ### Added @@ -134,7 +143,6 @@ All notable changes to this project will be documented in this file. ### Added - **πŸš€ Cloud Deployment CLI** - Complete automated deployment system for Supervaizer agents - - Full implementation of [RFC-001: Cloud Deployment CLI](docs/rfc/001-cloud-deployment-cli.md) - Support for three major cloud platforms: - **Google Cloud Run** with Artifact Registry and Secret Manager @@ -155,12 +163,10 @@ All notable changes to this project will be documented in this file. - See [Local Testing Documentation](docs/LOCAL_TESTING.md) for details - **Agent Instructions Template** - New HTML page served by FastAPI for Supervaize integration instructions - - Accessible at `/admin/supervaize-instructions` - Provides step-by-step setup guide for agents - **Version Check Utility** - Automatic check for latest Supervaizer version - - Helps users stay up-to-date with latest features and fixes - Located in `supervaizer.utils.version_check` @@ -172,7 +178,6 @@ All notable changes to this project will be documented in this file. ### Changed - **πŸ”„ Protocol Unification** - Removed ACP protocol in favor of unified A2A protocol - - Removed `src/supervaizer/protocol/acp/` directory and all ACP-specific code - Removed `acp_endpoints` parameter from Server class - Removed ACP route registration and test files @@ -182,7 +187,6 @@ All notable changes to this project will be documented in this file. - **Breaking Change**: `acp_endpoints` parameter no longer accepted in Server initialization - **πŸ“¦ Dependency Optimization** - Cloud SDKs moved to optional dependencies - - Base package size significantly reduced - Cloud deployment dependencies now optional: `pip install supervaizer[deploy]` - Optional `deploy` group includes: boto3, docker, google-cloud-artifact-registry, google-cloud-run, google-cloud-secret-manager, psutil @@ -242,7 +246,6 @@ All notable changes to this project will be documented in this file. ### Changed - **Parameter Validation System**: Refactored to provide separate validation endpoints for different concerns - - **Agent Parameters**: Now validated separately through `/validate-agent-parameters` endpoint - **Method Fields**: Now validated separately through `/validate-method-fields` endpoint - **Clean Architecture**: Removed legacy endpoint for cleaner, more focused API design diff --git a/docs/CLI.md b/docs/CLI.md index 6ace6cf..fdeb95b 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -58,8 +58,19 @@ supervaizer start --debug --reload # Set log level supervaizer start --log-level=DEBUG + +# Run in local mode (no Studio registration) +supervaizer start --local ``` +**Local mode (`--local`):** + +Starts the server without connecting to Studio. Your agents from `supervaizer_control.py` run alongside a built-in Hello World agent. If no `supervaizer_control.py` exists, only Hello World is loaded. + +- `SUPERVAIZER_LOCAL_MODE=true` is set automatically +- API key defaults to `local-dev` (override with `SUPERVAIZER_API_KEY`) +- Set `SUPERVAIZER_DISABLE_HELLO_WORLD=true` to deactivate the Hello World agent + ### deploy Automated deployment to cloud platforms. Requires installation with deploy extras: `pip install supervaizer[deploy]` @@ -246,6 +257,8 @@ All CLI options can also be configured through environment variables: | SUPERVAIZER_FORCE_INSTALL | Force overwrite existing file | false | | SUPERVAIZER_PRIVATE_KEY | RSA private key (PEM string) | generated at runtime if unset | | SUPERVAIZER_SERVER_ID | Stable server instance ID (UUID) | generated at runtime if unset | +| SUPERVAIZER_LOCAL_MODE | Enable local mode (true/false) | false | +| SUPERVAIZER_DISABLE_HELLO_WORLD | Disable built-in Hello World agent in local mode | false | **Serverless (e.g. Vercel):** On serverless platforms, each instance may be a new process. Set `SUPERVAIZER_PRIVATE_KEY` and `SUPERVAIZER_SERVER_ID` in the platform's environment variables so the same key and ID are used across instances. Otherwise every cold start generates a new key and server ID. diff --git a/docs/superpowers/plans/2026-03-16-manage-hello-world.md b/docs/superpowers/plans/2026-03-16-manage-hello-world.md new file mode 100644 index 0000000..2421c42 --- /dev/null +++ b/docs/superpowers/plans/2026-03-16-manage-hello-world.md @@ -0,0 +1,481 @@ +# Manage Hello World in Local Mode β€” Implementation Plan + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `supervaizer start --local` run user agents alongside the Hello World agent, with the ability to disable Hello World via env var. + +**Architecture:** Unify the `--local` and normal CLI code paths. The CLI sets `SUPERVAIZER_LOCAL_MODE=true` and falls through to the normal subprocess flow. `Server.__init__` detects local mode, forces `supervisor_account=None`, injects Hello World, and defaults `api_key` to `"local-dev"`. Also deploy agent routes in local mode (currently only deployed when `supervisor_account` is set). + +**Tech Stack:** Python, Typer (CLI), FastAPI (Server), pytest + +**Spec:** `docs/superpowers/specs/2026-03-16-manage-hello-world-design.md` + +--- + +## Chunk 1: Server local mode support + +### Task 1: Server injects Hello World agent in local mode + +**Files:** +- Modify: `src/supervaizer/server.py:306-460` (Server.__init__) +- Test: `tests/test_server.py` + +- [ ] **Step 1: Write failing tests for local mode Hello World injection** + +In `tests/test_server.py`, add: + +```python +class TestServerLocalMode: + """Tests for SUPERVAIZER_LOCAL_MODE behavior in Server.__init__.""" + + def test_local_mode_injects_hello_world_agent(self, agent_fixture: Agent) -> None: + """When SUPERVAIZER_LOCAL_MODE=true, Hello World agent is prepended.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 2 + assert server.agents[0].name == "Hello World AI Agent" + assert server.agents[1].name == agent_fixture.name + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_skips_hello_world_when_disabled(self, agent_fixture: Agent) -> None: + """When SUPERVAIZER_DISABLE_HELLO_WORLD=true, Hello World is not injected.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + assert server.agents[0].name == agent_fixture.name + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + del os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] + + def test_local_mode_skips_duplicate_hello_world(self) -> None: + """If user already has an agent with Hello World slug, skip injection.""" + from supervaizer.examples.local_server import get_default_local_agent + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + hw_agent = get_default_local_agent() + server = Server( + agents=[hw_agent], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_forces_supervisor_account_none( + self, agent_fixture: Agent, account_fixture: Any + ) -> None: + """When local mode is on, supervisor_account is forced to None.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + supervisor_account=account_fixture, + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert server.supervisor_account is None + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_defaults_api_key_to_local_dev(self, agent_fixture: Agent) -> None: + """In local mode without explicit api_key, default to 'local-dev'.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + # Clear any existing API key env var + old_key = os.environ.pop("SUPERVAIZER_API_KEY", None) + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key=None, + ) + assert server.api_key == "local-dev" + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + if old_key is not None: + os.environ["SUPERVAIZER_API_KEY"] = old_key + + def test_local_mode_deploys_agent_routes(self, agent_fixture: Agent) -> None: + """In local mode, agent routes are deployed even without supervisor_account.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + # Verify agent endpoints are reachable via TestClient + client = TestClient(server.app) + response = client.get(f"/supervaizer{agent_fixture.path}/") + assert response.status_code != 404 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_empty_server_still_starts(self) -> None: + """Local mode with Hello World disabled and no agents starts an empty server.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] = "true" + try: + server = Server( + agents=[], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 0 + # Admin UI should still be accessible + client = TestClient(server.app) + response = client.get("/admin/", headers={"X-API-Key": "test-key"}) + assert response.status_code != 404 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + del os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] + + def test_non_local_mode_unchanged(self, agent_fixture: Agent) -> None: + """Without local mode, Server behaves as before (no Hello World injection).""" + os.environ.pop("SUPERVAIZER_LOCAL_MODE", None) + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + assert server.agents[0].name == agent_fixture.name +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest tests/test_server.py::TestServerLocalMode -v` +Expected: FAIL (class doesn't exist yet, tests reference new behavior) + +- [ ] **Step 3: Implement local mode in Server.__init__** + +In `src/supervaizer/server.py`, at the top of `Server.__init__` (after the method signature, before mac_addr logic), add: + +```python +# Local mode: skip Studio, inject Hello World, default api_key +local_mode = os.environ.get("SUPERVAIZER_LOCAL_MODE", "").lower() == "true" +if local_mode: + if supervisor_account is not None: + log.warning( + "[Server] Local mode active β€” ignoring supervisor_account (no Studio registration)" + ) + supervisor_account = None + a2a_endpoints = True + admin_interface = True + if api_key is None: + api_key = "local-dev" + + # Inject Hello World agent unless disabled or duplicate + if os.environ.get("SUPERVAIZER_DISABLE_HELLO_WORLD", "").lower() != "true": + from supervaizer.examples.local_server import get_default_local_agent + hw_agent = get_default_local_agent() + existing_slugs = {a.slug for a in agents} + if hw_agent.slug not in existing_slugs: + agents = [hw_agent] + list(agents) + elif not agents: + log.warning( + "[Server] Local mode with Hello World disabled and no agents β€” server will be empty" + ) +``` + +Then in the route setup section (around line 440), change: + +```python +# Before: +if self.supervisor_account: + log.info(...) + self.app.include_router(create_default_routes(self)) + self.app.include_router(create_utils_routes(self)) + self.app.include_router(create_agents_routes(self)) + self.a2a_endpoints = True + +# After: +if self.supervisor_account or local_mode: + log.info( + "[Server launch] πŸš€ Deploy Supervaizer routes" + + (" (local mode)" if local_mode else " - also activates A2A routes") + ) + self.app.include_router(create_default_routes(self)) + self.app.include_router(create_utils_routes(self)) + self.app.include_router(create_agents_routes(self)) + self.a2a_endpoints = True +``` + +Note: `local_mode` needs to be accessible in the route setup section. Since it's defined at the top of `__init__`, it's already in scope. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest tests/test_server.py::TestServerLocalMode -v` +Expected: All 9 tests PASS + +- [ ] **Step 5: Run full test suite to check for regressions** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest tests/test_server.py -v` +Expected: All existing tests still PASS + +- [ ] **Step 6: Commit** + +```bash +but commit -m "feat: add local mode support to Server (Hello World injection, skip registration)" +``` + +--- + +## Chunk 2: CLI unification + +### Task 2: Unify CLI --local with normal subprocess path + +**Files:** +- Modify: `src/supervaizer/cli.py:108-217` +- Modify: `src/supervaizer/examples/local_server.py` +- Test: `tests/test_cli.py` + +- [ ] **Step 1: Write failing tests for unified CLI --local** + +In `tests/test_cli.py`, add to `TestCLIStart`: + +```python +def test_start_local_sets_env_and_runs_script( + self, runner: CliRunner, temp_script: str +) -> None: + """--local sets SUPERVAIZER_LOCAL_MODE and runs the script normally.""" + with patch("subprocess.Popen") as mock_popen: + mock_process = Mock() + mock_process.wait.return_value = 0 + mock_popen.return_value = mock_process + result = runner.invoke(app, ["start", "--local", temp_script]) + assert "local test mode" in result.stdout + mock_popen.assert_called_once() + # The env var should be set for the subprocess + assert os.environ.get("SUPERVAIZER_LOCAL_MODE") == "true" + +def test_start_local_without_script_uses_fallback(self, runner: CliRunner) -> None: + """--local without script_path and no supervaizer_control.py uses fallback.""" + with patch("subprocess.Popen") as mock_popen: + mock_process = Mock() + mock_process.wait.return_value = 0 + mock_popen.return_value = mock_process + # Run from a temp dir where no supervaizer_control.py exists + with patch("supervaizer.cli.os.path.exists", return_value=False): + result = runner.invoke(app, ["start", "--local"]) + assert "local test mode" in result.stdout + mock_popen.assert_called_once() + # Should use the fallback script path + call_args = mock_popen.call_args[0][0] + assert "local_server.py" in call_args[1] +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest tests/test_cli.py::TestCLIStart::test_start_local_sets_env_and_runs_script tests/test_cli.py::TestCLIStart::test_start_local_without_script_uses_fallback -v` +Expected: FAIL + +- [ ] **Step 3: Simplify local_server.py to a runnable fallback script** + +Rewrite `src/supervaizer/examples/local_server.py`: + +```python +"""Local test server fallback: used by `supervaizer start --local` when no supervaizer_control.py exists. + +Also exports get_default_local_agent() for Server to import when injecting Hello World. +""" + +import os +import shortuuid +from typing import Optional + +from supervaizer import ( + Agent, + AgentMethod, + AgentMethods, + ParametersSetup, + Parameter, + Server, +) +from supervaizer.agent import AgentMethodField + + +def get_default_local_agent() -> Agent: + """Default Hello World agent for local test mode (mirrors supervaize_hello_world).""" + # ... keep existing implementation exactly as-is ... + + +if __name__ == "__main__": + """Fallback entry point: starts a server with no user agents. + Hello World is injected by Server.__init__ when SUPERVAIZER_LOCAL_MODE=true. + """ + server = Server( + agents=[], + supervisor_account=None, + a2a_endpoints=True, + admin_interface=True, + host=os.environ.get("SUPERVAIZER_HOST") or "0.0.0.0", + port=int(os.environ.get("SUPERVAIZER_PORT") or "8000"), + public_url=os.environ.get("SUPERVAIZER_PUBLIC_URL"), + debug=os.environ.get("SUPERVAIZER_DEBUG", "False").lower() == "true", + reload=os.environ.get("SUPERVAIZER_RELOAD", "False").lower() == "true", + environment=os.environ.get("SUPERVAIZER_ENVIRONMENT", "dev"), + # api_key defaults to "local-dev" in Server.__init__ when SUPERVAIZER_LOCAL_MODE=true + ) + log_level = os.environ.get("SUPERVAIZER_LOG_LEVEL", "INFO") + server.launch(log_level=log_level) +``` + +Remove `create_local_server()` β€” it's no longer needed. + +- [ ] **Step 4: Rewrite the CLI start command** + +In `src/supervaizer/cli.py`, replace lines 164-217 (the `if local:` branch and the normal branch) with a unified flow: + +```python + if local: + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + console.print( + f"[bold green]Starting Supervaizer Controller v{VERSION}[/] (local test mode)" + ) + console.print( + "[dim]No Studio registration β€” agents run locally[/]" + ) + api_key = os.environ.get("SUPERVAIZER_API_KEY") or "local-dev" + base = public_url or f"http://{host}:{port}" + console.print( + f"[bold]API:[/] {base}/docs [bold]Admin/Workbench:[/] {base}/admin/" + ) + console.print( + f"[dim]API key for /admin: {api_key}[/]" + ) + + if script_path is None: + script_path = ( + os.environ.get("SUPERVAIZER_SCRIPT_PATH") or "supervaizer_control.py" + ) + + if not os.path.exists(script_path): + if local: + # Use fallback script (Hello World only) + import supervaizer.examples.local_server as fallback_module + script_path = fallback_module.__file__ + else: + console.print(f"[bold red]Error:[/] {script_path} not found") + console.print("Run [bold]supervaizer scaffold[/] to create a default script") + sys.exit(1) + + if not local: + console.print(f"[bold green]Starting Supervaizer Controller v{VERSION}[/]") + console.print(f"Loading configuration from [bold]{script_path}[/]") + + # Execute the script in a new Python process with proper signal handling + def signal_handler(signum: int, frame: Any) -> None: + if "process" in globals(): + globals()["process"].terminate() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + process = subprocess.Popen([sys.executable, script_path]) + globals()["process"] = process + process.wait() +``` + +Also update the `script_path` help text: +```python + script_path: Optional[str] = typer.Argument( + None, + help="Path to the supervaizer_control.py script", + ), +``` + +And update `--local` help text: +```python + local: bool = typer.Option( + False, + "--local", + help="Local test mode: run without Studio credentials, with built-in Hello World agent", + ), +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest tests/test_cli.py -v` +Expected: All tests PASS (new and existing) + +- [ ] **Step 6: Run full test suite** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && uv run pytest -v` +Expected: All tests PASS + +- [ ] **Step 7: Run pre-commit checks** + +Run: `cd /Volumes/SSDext1TB/Documents/GitRepo/RUNWAIZE/supervaizer && just precommit` +Expected: All checks pass (ruff, mypy, etc.) + +- [ ] **Step 8: Commit** + +```bash +but commit -m "feat: unify --local CLI path with normal subprocess flow" +``` + +--- + +## Chunk 3: Verification + +### Task 3: Manual smoke test + +- [ ] **Step 1: Test --local without supervaizer_control.py** + +Run from a directory without `supervaizer_control.py`: +```bash +cd /tmp && uv run supervaizer start --local +``` +Expected: Server starts with Hello World agent. Admin at `/admin/` shows Hello World. + +- [ ] **Step 2: Test --local with supervaizer_control.py** + +Run from the supervaize_hello_world example directory (or any dir with a `supervaizer_control.py`): +```bash +supervaizer start --local +``` +Expected: Server starts with user agents + Hello World. Both visible in admin. + +- [ ] **Step 3: Test SUPERVAIZER_DISABLE_HELLO_WORLD** + +```bash +SUPERVAIZER_DISABLE_HELLO_WORLD=true supervaizer start --local +``` +Expected: Server starts with user agents only, no Hello World. + +- [ ] **Step 4: Test normal mode (no --local) is unchanged** + +```bash +supervaizer start +``` +Expected: Behaves exactly as before (subprocess, Studio registration, no Hello World injection). diff --git a/docs/superpowers/specs/2026-03-16-manage-hello-world-design.md b/docs/superpowers/specs/2026-03-16-manage-hello-world-design.md new file mode 100644 index 0000000..cfd155d --- /dev/null +++ b/docs/superpowers/specs/2026-03-16-manage-hello-world-design.md @@ -0,0 +1,63 @@ +# Design: `--local` Mode with User Agents + Hello World + +## Problem + +`supervaizer start --local` currently ignores `supervaizer_control.py` entirely and runs a separate code path with only the built-in Hello World agent. Users with existing agent setups cannot see their agents alongside Hello World in local mode. + +## Goal + +Make `--local` behave like normal mode (same script loading, same server startup) with two differences: + +1. Skip Studio registration (no `supervisor_account` needed) +2. Inject the Hello World agent alongside user agents (controllable via env var) + +## Design + +### 1. `cli.py` β€” Unify the code paths + +Remove the separate `if local:` branch. When `--local` is set: + +- Set `SUPERVAIZER_LOCAL_MODE=true` as an environment variable +- Update `script_path` help text (remove "ignored when --local") +- If `script_path` is provided or `supervaizer_control.py` exists in CWD, fall through to the normal subprocess path +- If no script exists, use the fallback script from `local_server.py` instead +- Print local mode banner (no Studio registration message, API key info) + +### 2. `Server` β€” Handle local mode during init/launch + +In `Server.__init__`, check `SUPERVAIZER_LOCAL_MODE`: + +- If `true`, force `supervisor_account=None` regardless of what was passed (log a warning if a non-None value was overridden) +- If `true` and `SUPERVAIZER_DISABLE_HELLO_WORLD` is not `true`, prepend the Hello World agent to the agents list (inside `__init__`, before route setup). Skip injection if an agent with the same slug already exists. +- Enable `admin_interface` and `a2a_endpoints` automatically in local mode +- Default `api_key` to `"local-dev"` in local mode (if not explicitly set or via `SUPERVAIZER_API_KEY`) + +### 3. `local_server.py` β€” Becomes a fallback script + +Repurpose `local_server.py` as a minimal runnable script that `cli.py` invokes when `--local` is used but no `supervaizer_control.py` exists. It creates a `Server` with an empty agents list and calls `launch()`. Hello World is injected by `Server.__init__`. + +Keep `get_default_local_agent()` as the canonical factory for the Hello World agent β€” `Server` imports it from here. + +### Edge Cases + +| Scenario | Result | +| -------------------------------------------------------------- | -------------------------------------------------------------------------- | +| `--local`, no `supervaizer_control.py` | Hello World only (like today) | +| `--local` + `supervaizer_control.py` | User agents + Hello World | +| `--local` + `SUPERVAIZER_DISABLE_HELLO_WORLD=true` + script | User agents only | +| `--local` + `SUPERVAIZER_DISABLE_HELLO_WORLD=true` + no script | Warn and start empty server (admin UI still accessible) | +| `--local` + script with real `supervisor_account` | Override to `None`, log warning that credentials are skipped in local mode | +| `--local` + user agent named "Hello World AI Agent" | Skip Hello World injection (no duplicate) | + +### Files to Modify + +1. `src/supervaizer/cli.py` β€” Remove separate local branch, set env var, unify paths, update help text +2. `src/supervaizer/server.py` β€” Add local mode detection, Hello World injection, skip registration, api_key default +3. `src/supervaizer/examples/local_server.py` β€” Simplify to fallback-only script, keep `get_default_local_agent()` + +### What Doesn't Change + +- `supervaizer_control.py` structure β€” works as-is +- Agent loading mechanism +- Route registration +- Admin workbench UI diff --git a/justfile b/justfile index 1dee62c..490d63a 100644 --- a/justfile +++ b/justfile @@ -144,4 +144,21 @@ release: just merge-to-main just push-main just push_tags + just gh-release @echo "βœ… Release complete! Main branch and tags pushed to remote" + +# Create GitHub release for the current version +gh-release: + #!/usr/bin/env bash + VERSION=$(grep '^VERSION = ' src/supervaizer/__version__.py | cut -d'"' -f2) + TAG="v${VERSION}" + echo "Creating GitHub release ${TAG}..." + PREV_TAG=$(git tag --sort=-creatordate | grep -v "^${TAG}$" | head -1) + NOTES=$(git log "${PREV_TAG}..${TAG}" --oneline --no-merges | grep -v "Bump version") + gh release create "${TAG}" \ + --repo supervaize/supervaizer \ + --title "${TAG}" \ + --latest \ + --generate-notes \ + --notes-start-tag "${PREV_TAG}" + echo "βœ… GitHub release ${TAG} created" diff --git a/src/supervaizer/admin/routes.py b/src/supervaizer/admin/routes.py index af65c4e..5d23925 100644 --- a/src/supervaizer/admin/routes.py +++ b/src/supervaizer/admin/routes.py @@ -414,7 +414,6 @@ async def serve_static(file_path: str) -> Response: media_type=media_types.get(suffix, "application/octet-stream"), ) - @router.get("/console", response_class=HTMLResponse) async def admin_console_page(request: Request) -> Response: """Interactive console page - publicly accessible, authentication handled by frontend.""" diff --git a/src/supervaizer/admin/workbench_routes.py b/src/supervaizer/admin/workbench_routes.py index 765eea6..26731be 100644 --- a/src/supervaizer/admin/workbench_routes.py +++ b/src/supervaizer/admin/workbench_routes.py @@ -419,27 +419,31 @@ async def workbench_answer_hitl( case.receive_human_input(update) # Step 2: Invoke agent's human_answer method if defined - if agent.methods and getattr(agent.methods, "human_answer", None): - human_answer_method = getattr(agent.methods, "human_answer", None).method - try: - params = { - "fields": answer_data, - "context": {"job_id": job_id, "case_id": case_id}, - "payload": answer_data, - "case_id": case_id, - "job_id": job_id, - } - loop = asyncio.get_running_loop() - await loop.run_in_executor( - None, - lambda: agent._execute(human_answer_method, params), - ) - except Exception as e: - log.error(f"[Workbench] human_answer failed for case {case_id}: {e}") - return JSONResponse( - {"status": "error", "message": f"human_answer failed: {e}"}, - status_code=500, - ) + if agent.methods: + human_answer_def = getattr(agent.methods, "human_answer", None) + if human_answer_def is not None: + human_answer_method = human_answer_def.method + try: + params = { + "fields": answer_data, + "context": {"job_id": job_id, "case_id": case_id}, + "payload": answer_data, + "case_id": case_id, + "job_id": job_id, + } + loop = asyncio.get_running_loop() + await loop.run_in_executor( + None, + lambda: agent._execute(human_answer_method, params), + ) + except Exception as e: + log.error( + f"[Workbench] human_answer failed for case {case_id}: {e}" + ) + return JSONResponse( + {"status": "error", "message": f"human_answer failed: {e}"}, + status_code=500, + ) return JSONResponse({ "status": "answered", diff --git a/src/supervaizer/case.py b/src/supervaizer/case.py index 4f8f468..eafc038 100644 --- a/src/supervaizer/case.py +++ b/src/supervaizer/case.py @@ -7,12 +7,10 @@ from datetime import datetime from enum import Enum -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional import shortuuid -from pydantic import ConfigDict, Field -from pydantic.json_schema import SkipJsonSchema -from typing import Callable +from pydantic import ConfigDict from supervaizer.common import SvBaseModel, log, singleton from supervaizer.lifecycle import EntityEvents, EntityStatus from supervaizer.storage import PersistentEntityLifecycle, StorageManager @@ -136,6 +134,12 @@ class CaseNodeType(Enum): class CaseNode(SvBaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) + name: str + type: CaseNodeType + factory: Optional[Callable[..., CaseNodeUpdate]] = None + description: str = "" + can_be_confirmed: bool = False + def __call__(self, *args: Any, **kwargs: Any) -> CaseNodeUpdate: """Make it callable directly.""" if self.factory is None: diff --git a/src/supervaizer/cli.py b/src/supervaizer/cli.py index 6d7e8c8..6493396 100644 --- a/src/supervaizer/cli.py +++ b/src/supervaizer/cli.py @@ -142,11 +142,11 @@ def start( local: bool = typer.Option( False, "--local", - help="Local test mode: run without Studio credentials, with built-in Hello World agent (for agent workbench)", + help="Local test mode: run without Studio credentials, with built-in Hello World agent", ), script_path: Optional[str] = typer.Argument( None, - help="Path to the supervaizer_control.py script (ignored when --local)", + help="Path to the supervaizer_control.py script", ), ) -> None: """Start the Supervaizer Controller server.""" @@ -162,31 +162,17 @@ def start( os.environ["SUPERVAIZER_PUBLIC_URL"] = public_url if local: + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" console.print( f"[bold green]Starting Supervaizer Controller v{VERSION}[/] (local test mode)" ) - console.print( - "[dim]No Studio registration β€” built-in Hello World agent only[/]" - ) - from supervaizer.examples.local_server import create_local_server - - server = create_local_server( - host=host, - port=port, - public_url=public_url, - debug=debug, - reload=reload, - environment=environment, - ) - base = server.public_url or f"http://{server.host}:{server.port}" + console.print("[dim]No Studio registration β€” agents run locally[/]") + api_key_display = os.environ.get("SUPERVAIZER_API_KEY") or "local-dev" + base = public_url or f"http://{host}:{port}" console.print( f"[bold]API:[/] {base}/docs [bold]Admin/Workbench:[/] {base}/admin/" ) - console.print( - "[dim]API key for /admin: local-dev (set SUPERVAIZER_API_KEY to override)[/]" - ) - server.launch(log_level=log_level) - return + console.print(f"[dim]API key for /admin: {api_key_display}[/]") if script_path is None: script_path = ( @@ -194,11 +180,20 @@ def start( ) if not os.path.exists(script_path): - console.print(f"[bold red]Error:[/] {script_path} not found") - console.print("Run [bold]supervaizer scaffold[/] to create a default script") - sys.exit(1) + if local: + # Use fallback script (Hello World only) + import supervaizer.examples.local_server as fallback_module + + script_path = fallback_module.__file__ + else: + console.print(f"[bold red]Error:[/] {script_path} not found") + console.print( + "Run [bold]supervaizer scaffold[/] to create a default script" + ) + sys.exit(1) - console.print(f"[bold green]Starting Supervaizer Controller v{VERSION}[/]") + if not local: + console.print(f"[bold green]Starting Supervaizer Controller v{VERSION}[/]") console.print(f"Loading configuration from [bold]{script_path}[/]") # Execute the script in a new Python process with proper signal handling diff --git a/src/supervaizer/examples/local_server.py b/src/supervaizer/examples/local_server.py index a550c10..6f31228 100644 --- a/src/supervaizer/examples/local_server.py +++ b/src/supervaizer/examples/local_server.py @@ -4,22 +4,21 @@ # If a copy of the MPL was not distributed with this file, you can obtain one at # https://mozilla.org/MPL/2.0/. -"""Local test server: no Studio registration, built-in Hello World agent. +"""Local test server fallback: used by `supervaizer start --local` when no supervaizer_control.py exists. -Use with `supervaizer start --local` to run the FastAPI server and agent workbench -without Supervaize Studio credentials. +Also exports get_default_local_agent() for Server to import when injecting Hello World. """ import os + import shortuuid -from typing import Any, Optional from supervaizer import ( Agent, AgentMethod, AgentMethods, - ParametersSetup, Parameter, + ParametersSetup, Server, ) from supervaizer.agent import AgentMethodField @@ -106,28 +105,22 @@ def get_default_local_agent() -> Agent: ) -def create_local_server( - host: Optional[str] = None, - port: Optional[int] = None, - public_url: Optional[str] = None, - debug: bool = False, - reload: bool = False, - environment: str = "dev", - api_key: Optional[str] = None, - **kwargs: Any, -) -> Server: - """Create a Server with no supervisor_account and the default Hello World agent.""" - return Server( - agents=[get_default_local_agent()], +if __name__ == "__main__": + # Fallback entry point: starts a server with no user agents. + # Hello World is injected by Server.__init__ when SUPERVAIZER_LOCAL_MODE=true. + server = Server( + agents=[], supervisor_account=None, a2a_endpoints=True, admin_interface=True, - host=host or os.environ.get("SUPERVAIZER_HOST") or "0.0.0.0", - port=port or int(os.environ.get("SUPERVAIZER_PORT") or "8000"), - public_url=public_url or os.environ.get("SUPERVAIZER_PUBLIC_URL"), - debug=debug, - reload=reload, - environment=environment, - api_key=api_key or os.environ.get("SUPERVAIZER_API_KEY") or "local-dev", - **kwargs, + host=os.environ.get("SUPERVAIZER_HOST") or "0.0.0.0", + port=int(os.environ.get("SUPERVAIZER_PORT") or "8000"), + public_url=os.environ.get("SUPERVAIZER_PUBLIC_URL"), + debug=os.environ.get("SUPERVAIZER_DEBUG", "False").lower() == "true", + reload=os.environ.get("SUPERVAIZER_RELOAD", "False").lower() == "true", + environment=os.environ.get("SUPERVAIZER_ENVIRONMENT", "dev"), + # Pass None so Server.__init__ local-mode logic defaults to "local-dev" + api_key=None, ) + log_level = os.environ.get("SUPERVAIZER_LOG_LEVEL", "INFO") + server.launch(log_level=log_level) diff --git a/src/supervaizer/server.py b/src/supervaizer/server.py index c7a3de5..7aaf7ba 100644 --- a/src/supervaizer/server.py +++ b/src/supervaizer/server.py @@ -9,6 +9,7 @@ import sys import time import uuid +from pathlib import Path from datetime import datetime from typing import Any, ClassVar, Dict, List, Optional, TypeVar, cast from urllib.parse import urlunparse @@ -128,7 +129,7 @@ def save_server_info_to_storage(server_instance: "Server") -> None: # Create server info server_info = ServerInfo( - id=getattr(server_instance, "server_id", "N/A"), + id="server_instance", host=getattr(server_instance, "host", "N/A"), port=getattr(server_instance, "port", 0), api_version=API_VERSION, @@ -139,7 +140,7 @@ def save_server_info_to_storage(server_instance: "Server") -> None: updated_at=datetime.now().isoformat(), ) - # Save to storage + # Save to storage under the fixed singleton id so retrieval works storage.save_object("ServerInfo", server_info.model_dump()) log.info( @@ -342,6 +343,36 @@ def __init__( api_key: API key for securing endpoints """ + # Local mode: skip Studio, inject Hello World, default api_key + local_mode = os.environ.get("SUPERVAIZER_LOCAL_MODE", "").lower() == "true" + if local_mode: + if supervisor_account is not None: + log.warning( + "[Server] Local mode active β€” ignoring supervisor_account" + " (no Studio registration)" + ) + supervisor_account = None + a2a_endpoints = True + admin_interface = True + if not os.environ.get("SUPERVAIZER_API_KEY"): + api_key = "local-dev" + + # Inject Hello World agent unless disabled or duplicate + if os.environ.get("SUPERVAIZER_DISABLE_HELLO_WORLD", "").lower() != "true": + from supervaizer.examples.local_server import ( + get_default_local_agent, + ) + + hw_agent = get_default_local_agent() + existing_slugs = {a.slug for a in agents} + if hw_agent.slug not in existing_slugs: + agents = [hw_agent] + list(agents) + elif not agents: + log.warning( + "[Server] Local mode with Hello World disabled and no" + " agents β€” server will be empty" + ) + if not mac_addr: node_id = uuid.getnode() mac_addr = "-".join( @@ -436,14 +467,15 @@ async def validation_exception_handler( log.info(f"[Server launch] Server ID: {self.server_id}") # Create routes - if self.supervisor_account: + if self.supervisor_account or local_mode: log.info( - "[Server launch] πŸš€ Deploy Supervaizer routes - also activates A2A routes" + "[Server launch] πŸš€ Deploy Supervaizer routes" + + (" (local mode)" if local_mode else " - also activates A2A routes") ) self.app.include_router(create_default_routes(self)) self.app.include_router(create_utils_routes(self)) self.app.include_router(create_agents_routes(self)) - self.a2a_endpoints = True # Needed by supervaize. + self.a2a_endpoints = True if self.a2a_endpoints: log.info("[Server launch] πŸ“’ Deploy A2A routes ") self.app.include_router(create_a2a_routes(self)) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7c5880f..4007c17 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -62,6 +62,32 @@ def test_start_with_default_script_missing(self, runner: CliRunner) -> None: assert result.exit_code == 1 assert "Error: supervaizer_control.py not found" in result.stdout + def test_start_local_sets_env_and_runs_script( + self, runner: CliRunner, temp_script: str + ) -> None: + """--local sets SUPERVAIZER_LOCAL_MODE and runs the script normally.""" + with patch("subprocess.Popen") as mock_popen: + mock_process = Mock() + mock_process.wait.return_value = 0 + mock_popen.return_value = mock_process + result = runner.invoke(app, ["start", "--local", temp_script]) + assert "local test mode" in result.stdout + mock_popen.assert_called_once() + assert os.environ.get("SUPERVAIZER_LOCAL_MODE") == "true" + + def test_start_local_without_script_uses_fallback(self, runner: CliRunner) -> None: + """--local without script_path and no supervaizer_control.py uses fallback.""" + with patch("subprocess.Popen") as mock_popen: + mock_process = Mock() + mock_process.wait.return_value = 0 + mock_popen.return_value = mock_process + with patch("supervaizer.cli.os.path.exists", return_value=False): + result = runner.invoke(app, ["start", "--local"]) + assert "local test mode" in result.stdout + mock_popen.assert_called_once() + call_args = mock_popen.call_args[0][0] + assert "local_server.py" in call_args[1] + class TestCLIInstall: """Tests for the scaffold command.""" diff --git a/tests/test_server.py b/tests/test_server.py index cbe1caf..614e931 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -569,3 +569,149 @@ def test_server_registration_info(server_fixture: Server) -> None: "environment": "test", "api_key": "test-api-key", } + + +class TestServerLocalMode: + """Tests for SUPERVAIZER_LOCAL_MODE behavior in Server.__init__.""" + + def test_local_mode_injects_hello_world_agent(self, agent_fixture: Agent) -> None: + """When SUPERVAIZER_LOCAL_MODE=true, Hello World agent is prepended.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 2 + assert server.agents[0].name == "Hello World AI Agent" + assert server.agents[1].name == agent_fixture.name + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_skips_hello_world_when_disabled( + self, agent_fixture: Agent + ) -> None: + """When SUPERVAIZER_DISABLE_HELLO_WORLD=true, Hello World is not injected.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + assert server.agents[0].name == agent_fixture.name + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + del os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] + + def test_local_mode_skips_duplicate_hello_world(self) -> None: + """If user already has an agent with Hello World slug, skip injection.""" + from supervaizer.examples.local_server import get_default_local_agent + + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + hw_agent = get_default_local_agent() + server = Server( + agents=[hw_agent], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_forces_supervisor_account_none( + self, agent_fixture: Agent, account_fixture: Any + ) -> None: + """When local mode is on, supervisor_account is forced to None.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + supervisor_account=account_fixture, + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert server.supervisor_account is None + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_defaults_api_key_to_local_dev( + self, agent_fixture: Agent + ) -> None: + """In local mode without SUPERVAIZER_API_KEY env var, default to 'local-dev'.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + old_key = os.environ.pop("SUPERVAIZER_API_KEY", None) + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + ) + assert server.api_key == "local-dev" + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + if old_key is not None: + os.environ["SUPERVAIZER_API_KEY"] = old_key + + def test_local_mode_deploys_agent_routes(self, agent_fixture: Agent) -> None: + """In local mode, agent routes are deployed even without supervisor_account.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + try: + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + client = TestClient(server.app) + response = client.get(f"/supervaizer{agent_fixture.path}/") + assert response.status_code != 404 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + + def test_local_mode_empty_server_still_starts(self) -> None: + """Local mode with Hello World disabled and no agents starts an empty server.""" + os.environ["SUPERVAIZER_LOCAL_MODE"] = "true" + os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] = "true" + try: + server = Server( + agents=[], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 0 + client = TestClient(server.app) + response = client.get("/admin/", headers={"X-API-Key": "test-key"}) + assert response.status_code != 404 + finally: + del os.environ["SUPERVAIZER_LOCAL_MODE"] + del os.environ["SUPERVAIZER_DISABLE_HELLO_WORLD"] + + def test_non_local_mode_unchanged(self, agent_fixture: Agent) -> None: + """Without local mode, Server behaves as before (no Hello World injection).""" + os.environ.pop("SUPERVAIZER_LOCAL_MODE", None) + server = Server( + agents=[agent_fixture], + host="localhost", + port=8002, + environment="test", + api_key="test-key", + ) + assert len(server.agents) == 1 + assert server.agents[0].name == agent_fixture.name From 99f79c59f0b4ccb6b3e79c936bc37b79530d66a8 Mon Sep 17 00:00:00 2001 From: "Alain@Runwaize" Date: Tue, 17 Mar 2026 19:58:45 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20feat(workbench):=20add=20poll?= =?UTF-8?q?=20button=20and=20ignore-cli=20a=20conditional=20"Check=20for?= =?UTF-8?q?=20updates"=20poll=20button=20to=20the=20workbench=20UI=20thati?= =?UTF-8?q?s=20rendered=20only=20when=20the=20agent=20exposes=20job=5Fpoll?= =?UTF-8?q?=20(has=5Fpoll).=20The=20buttoninvokes=20workbenchForm.pollJob(?= =?UTF-8?q?)=20and=20is=20styled=20to=20match=20controlsThis=20enables=20m?= =?UTF-8?q?anual=20polling=20for=20job=20status/updates=20when=20the=20bac?= =?UTF-8?q?kend=20supportsitAlso=20update=20.gitignore=20to=20exclude=20.w?= =?UTF-8?q?right-cli/=20tocommittingwright=20CLI=20artifacts.=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src/supervaizer/admin/templates/workbench.html | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/.gitignore b/.gitignore index 10075b3..edc0cbe 100644 --- a/.gitignore +++ b/.gitignore @@ -252,3 +252,4 @@ uv.lock .pypirc .deployment/ +.playwright-cli/ \ No newline at end of file diff --git a/src/supervaizer/admin/templates/workbench.html b/src/supervaizer/admin/templates/workbench.html index 3a4d8e0..95c184e 100644 --- a/src/supervaizer/admin/templates/workbench.html +++ b/src/supervaizer/admin/templates/workbench.html @@ -160,6 +160,21 @@

Jo Start Job + {# Poll button β€” visible only when agent has job_poll #} + {% if has_poll %} + + {% endif %} + {# Stop button #} + + + + +{% endmacro %} + + +{# Confirmed content display β€” shown after dialog is approved #} +{% macro render_dialog_confirmed(content_raw, content_type) %} +
+
+ + + Approved Content + +
+ {% if content_type == "email" %} +
+ + +
+ {% else %} +
{{ content_raw }}
+ {% endif %} +
+{% endmacro %} diff --git a/src/supervaizer/admin/templates/components/field_renderer.html b/src/supervaizer/admin/templates/components/field_renderer.html index e2bca5f..9bb29af 100644 --- a/src/supervaizer/admin/templates/components/field_renderer.html +++ b/src/supervaizer/admin/templates/components/field_renderer.html @@ -14,14 +14,15 @@ #} {% macro render_field(field, prefix="field_") %} -
-