Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,6 @@ uv.lock
.pypirc

.deployment/
.playwright-cli/
/
but/
33 changes: 33 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,39 @@ All notable changes to this project will be documented in this file.

## Unreleased

### v0.11.0

- **Job Poll mechanism** — New optional `job_poll` method in `AgentMethods` for manual external service polling. When defined, the workbench shows a "Check for updates" button on active jobs. Clicking it calls the agent's poll handler, which checks external services (email inboxes, call status APIs, etc.) and updates cases accordingly. Enables local development without webhooks — production uses real-time webhooks, local mode uses the poll button.
- `AgentMethods`: new `job_poll: AgentMethod | None` field
- Route: `POST /workbench/jobs/{job_id}/poll` triggers the agent's poll handler
- UI: "Check for updates" button with loading state, conditionally rendered via `has_poll` template variable
- JS: `pollJob()` in `workbench-form.js` with disable-during-request pattern

- **HITL double-click guard** — All HITL buttons are disabled during submission via `_postAnswer()`. Buttons get `opacity-50 cursor-not-allowed` while in flight and re-enable on error. Dialog HITL `submitMessage`/`confirmDialog` are now async, properly awaiting the form submission before resetting state.

- **Monitor reply display** — Step payloads with a `reply` key render as a blue blockquote below the step row. Step payloads with `approved_content` render as a green confirmed card.

- **Monitor duration display** — `step_duration` format now handles both numeric and string values, preventing Jinja2 `TypeError` when duration comes from HITL form input.

- **WebSocket terminal signal** — Terminal job state sends a `terminal` WS signal that keeps the connection idle, preventing reconnect loops.

- **`is_local_mode()` helper** — Centralized in `supervaizer.common`, replacing inline `os.environ.get("SUPERVAIZER_LOCAL_MODE")` checks across `account_service.py`, `server.py`.

- **`deque` for log buffer** — `_workbench_log_buffer` changed from `list` to `collections.deque(maxlen=500)`, removing manual trim logic.

- **Dialog HITL** — New HITL type for interactive content review via chat interface. When a `CaseNodeUpdate` payload contains `supervaizer_dialog`, the workbench renders a conversation UI instead of a fixed form. Supports iterative refinement through LLM-powered feedback loops. Fields: `content` (JSON string or plain text), `content_type` (email/text/code), `objective`, `instructions`, `messages` (conversation history), `confirm_label`.
- Template: `dialog_renderer.html` with `render_hitl_dialog` and `render_dialog_confirmed` macros
- JS: `submitDialogMessage(caseId, message)` and `confirmDialog(caseId)` in `workbench-form.js`
- Route: `workbench_routes.py` detects `supervaizer_dialog` in AWAITING case payloads

- **Local mode URL fixes** — `supervaizer start --local --port N` now correctly shows localhost URLs everywhere: CLI output, admin interface logs, storage, and uvicorn. Fixed by resolving `Server.__init__` defaults from env vars at call time (not class definition time) and ensuring CLI-provided values take precedence over `.env` file values.

- **Local mode event skipping** — `account_service.send_event()` returns a no-op `ApiSuccess` when `SUPERVAIZER_LOCAL_MODE=true`, preventing HTTP errors against the SaaS API during local development.

- **Agent parameter env pre-fill** — In local mode, the workbench auto-loads `.env` values into agent parameter fields with green `.env` badge indicators. Secret fields show a masked placeholder; non-secret fields display the value. Backend falls back to env values for empty fields on job submission.

- **HTMX polling guard** — Monitor template suppresses `hx-trigger` polling when a HITL dialog or form is active, preventing DOM overwrites while users interact with forms.

### 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.
Expand Down
58 changes: 45 additions & 13 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,50 @@ release:
just gh-release
@echo "✅ Release complete! Main branch and tags pushed to remote"

# Create GitHub release for the current version
# Create or update GitHub release for the latest tag on origin/main
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"
set -euo pipefail

echo "Fetching latest tags from origin..."
git fetch origin --tags

# Find the latest tag reachable from origin/main
LATEST_TAG=$(git describe --tags --abbrev=0 origin/main)

if [ -z "${LATEST_TAG}" ]; then
echo "❌ No tags found on origin/main. Aborting."
exit 1
fi

echo "Using latest tag on origin/main: ${LATEST_TAG}"

# Find previous tag (for release notes range)
PREV_TAG=$(git tag --merged origin/main --sort=-creatordate | grep -v "^${LATEST_TAG}$" | head -1 || true)

# If a release already exists, just mark it as latest; otherwise create it
if gh release view "${LATEST_TAG}" --repo supervaize/supervaizer >/dev/null 2>&1; then
echo "GitHub release ${LATEST_TAG} already exists. Marking as latest..."
gh release edit "${LATEST_TAG}" \
--repo supervaize/supervaizer \
--latest
echo "✅ GitHub release ${LATEST_TAG} updated as latest"
else
echo "Creating GitHub release ${LATEST_TAG}..."
if [ -n "${PREV_TAG}" ]; then
gh release create "${LATEST_TAG}" \
--repo supervaize/supervaizer \
--title "${LATEST_TAG}" \
--latest \
--generate-notes \
--notes-start-tag "${PREV_TAG}"
else
# First release: no previous tag
gh release create "${LATEST_TAG}" \
--repo supervaize/supervaizer \
--title "${LATEST_TAG}" \
--latest \
--generate-notes
fi
echo "✅ GitHub release ${LATEST_TAG} created"
fi
9 changes: 8 additions & 1 deletion src/supervaizer/account_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import httpx

from supervaizer.common import ApiError, ApiResult, ApiSuccess, log
from supervaizer.common import ApiError, ApiResult, ApiSuccess, is_local_mode, log

logger = logging.getLogger("httpx")
# Enable httpx debug logging (optional - uncomment for transport-level debugging)
Expand Down Expand Up @@ -57,6 +57,13 @@ def send_event(
Tested in tests/test_account_service.py
"""

# In local mode, skip sending events to the SaaS API entirely.
if is_local_mode():
log.debug(f"[Send event] Local mode — skipping {event.type.name}")
return ApiSuccess(
message=f"Event {event.type.name} skipped (local mode)", detail=None
)

headers = account.api_headers
payload = event.payload
url_event = (
Expand Down
7 changes: 6 additions & 1 deletion src/supervaizer/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1223,12 +1223,17 @@ async def execute_console_command(
return {"status": "error", "message": str(e)}

# Include workbench sub-router
from supervaizer.admin.workbench_routes import create_workbench_routes
from supervaizer.admin.workbench_routes import (
create_workbench_routes,
create_workbench_ws_routes,
)

router.include_router(
create_workbench_routes(),
dependencies=[Depends(verify_admin_access)],
)
# WebSocket routes are mounted separately — WS can't use APIKeyHeader auth
router.include_router(create_workbench_ws_routes())

return router

Expand Down
Loading
Loading