diff --git a/.github/workflows/build-tui.yml b/.github/workflows/build-tui.yml index dffd7cb..0314dcc 100644 --- a/.github/workflows/build-tui.yml +++ b/.github/workflows/build-tui.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - AGENTSWARM_CLI_VERSION: 1.4.34 + AGENTSWARM_CLI_VERSION: 1.4.35 jobs: prepare: @@ -111,15 +111,48 @@ jobs: - os: macos-latest platform: darwin arch: arm64 + baseline: false + asset: agentswarm-darwin-arm64 - os: macos-15-intel platform: darwin arch: x64 + baseline: false + asset: agentswarm-darwin-x64 + - os: macos-15-intel + platform: darwin + arch: x64 + baseline: true + asset: agentswarm-darwin-x64-baseline - os: ubuntu-latest platform: linux arch: x64 + baseline: false + asset: agentswarm-linux-x64 + - os: ubuntu-latest + platform: linux + arch: x64 + baseline: true + asset: agentswarm-linux-x64-baseline + - os: ubuntu-24.04-arm + platform: linux + arch: arm64 + baseline: false + asset: agentswarm-linux-arm64 + - os: windows-latest + platform: windows + arch: x64 + baseline: false + asset: agentswarm-windows-x64.exe - os: windows-latest platform: windows arch: x64 + baseline: true + asset: agentswarm-windows-x64-baseline.exe + - os: windows-11-arm + platform: windows + arch: arm64 + baseline: false + asset: agentswarm-windows-arm64.exe runs-on: ${{ matrix.os }} steps: @@ -151,28 +184,33 @@ jobs: AGENTSWARM_PRODUCT_MDNS_DOMAIN: openswarm.local AGENTSWARM_PRODUCT_STARTER_REPO: VRSEN/OpenSwarm AGENTSWARM_PRODUCT_STARTER_FOLDER: openswarm - AGENTSWARM_PRODUCT_ENTRY_FILES: swarm.py,agency.py + AGENTSWARM_PRODUCT_ENTRY_FILES: swarm.py + AGENTSWARM_PRODUCT_SKIP_POST_AUTH_MODEL_SELECTION: "true" + AGENTSWARM_PRODUCT_TUI_LOGO_LEFT: '[" "," ██████╗ ██████╗ ███████╗███╗ ██╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║","╚██████╔╝██║ ███████╗██║ ╚████║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝"]' + AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT: '["","███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║","███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║","╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_WORDMARK_LINES: '[""," ██████╗ ██████╗ ███████╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ╚════██║██║███╗██║██╔══██╗██╔══██╗██║╚██╔╝██║","╚██████╔╝██║ ███████╗██║ ╚████║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_PYTHON_ENVIRONMENT: standalone AGENTSWARM_PRODUCT_VERSION: ${{ needs.prepare.outputs.release_version }} - run: bun run script/build.ts --single --skip-install + run: bun run script/build.ts --single --skip-install ${{ matrix.baseline && '--baseline' || '' }} - name: Rename binary (Unix) if: matrix.platform != 'windows' run: | - mv agentswarm-cli/packages/opencode/dist/agentswarm-cli-${{ matrix.platform }}-${{ matrix.arch }}/bin/agentswarm \ - agentswarm-${{ matrix.platform }}-${{ matrix.arch }} + mv agentswarm-cli/packages/opencode/dist/agentswarm-cli-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.baseline && '-baseline' || '' }}/bin/agentswarm \ + ${{ matrix.asset }} - name: Rename binary (Windows) if: matrix.platform == 'windows' shell: pwsh run: | - Move-Item agentswarm-cli/packages/opencode/dist/agentswarm-cli-${{ matrix.platform }}-${{ matrix.arch }}/bin/agentswarm.exe ` - agentswarm-${{ matrix.platform }}-${{ matrix.arch }}.exe + Move-Item agentswarm-cli/packages/opencode/dist/agentswarm-cli-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.baseline && '-baseline' || '' }}/bin/agentswarm.exe ` + ${{ matrix.asset }} - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: agentswarm-${{ matrix.platform }}-${{ matrix.arch }} - path: agentswarm-${{ matrix.platform }}-${{ matrix.arch }}${{ matrix.platform == 'windows' && '.exe' || '' }} + name: ${{ matrix.asset }} + path: ${{ matrix.asset }} if-no-files-found: error release: @@ -285,8 +323,13 @@ jobs: required_assets=( agentswarm-darwin-arm64 agentswarm-darwin-x64 + agentswarm-darwin-x64-baseline + agentswarm-linux-arm64 agentswarm-linux-x64 + agentswarm-linux-x64-baseline + agentswarm-windows-arm64.exe agentswarm-windows-x64.exe + agentswarm-windows-x64-baseline.exe ) for required in "${required_assets[@]}"; do diff --git a/.github/workflows/live-run-mode-smoke.yml b/.github/workflows/live-run-mode-smoke.yml index 8ab5d09..848a6dc 100644 --- a/.github/workflows/live-run-mode-smoke.yml +++ b/.github/workflows/live-run-mode-smoke.yml @@ -29,11 +29,55 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + - uses: actions/checkout@v4 + with: + repository: VRSEN/agentswarm-cli + ref: v1.4.35 + path: agentswarm-cli + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.13" + - name: Resolve OpenSwarm package version + id: openswarm + run: node -e "console.log('version=' + require('./package.json').version)" >> "$GITHUB_OUTPUT" + - name: Build local OpenSwarm TUI + shell: bash + working-directory: agentswarm-cli + env: + OPENCODE_CHANNEL: rc + AGENTSWARM_PRODUCT_DISPLAY_NAME: OpenSwarm + AGENTSWARM_PRODUCT_COMMAND: openswarm + AGENTSWARM_PRODUCT_PACKAGE_NAME: "@vrsen/openswarm" + AGENTSWARM_PRODUCT_LAUNCHER_PACKAGE_NAME: "@vrsen/openswarm" + AGENTSWARM_PRODUCT_RELEASE_REPO: VRSEN/OpenSwarm + AGENTSWARM_PRODUCT_DOCS_URL: https://github.com/VRSEN/OpenSwarm + AGENTSWARM_PRODUCT_ISSUE_URL: https://github.com/VRSEN/OpenSwarm/issues/new?template=bug-report.yml + AGENTSWARM_PRODUCT_MDNS_DOMAIN: openswarm.local + AGENTSWARM_PRODUCT_STARTER_REPO: VRSEN/OpenSwarm + AGENTSWARM_PRODUCT_STARTER_FOLDER: openswarm + AGENTSWARM_PRODUCT_ENTRY_FILES: swarm.py + AGENTSWARM_PRODUCT_SKIP_POST_AUTH_MODEL_SELECTION: "true" + AGENTSWARM_PRODUCT_TUI_LOGO_LEFT: '[" "," ██████╗ ██████╗ ███████╗███╗ ██╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║","╚██████╔╝██║ ███████╗██║ ╚████║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝"]' + AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT: '["","███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║","███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║","╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_WORDMARK_LINES: '[""," ██████╗ ██████╗ ███████╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ╚════██║██║███╗██║██╔══██╗██╔══██╗██║╚██╔╝██║","╚██████╔╝██║ ███████╗██║ ╚████║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_PYTHON_ENVIRONMENT: standalone + AGENTSWARM_PRODUCT_VERSION: ${{ steps.openswarm.outputs.version }} + run: | + bun install + cd packages/opencode + bun run script/build.ts --single --skip-install + binary="$(find dist -path '*/bin/agentswarm' -type f -perm -111 | head -n 1)" + if [ -z "$binary" ]; then + echo "FAILED: built agentswarm binary not found under agentswarm-cli/packages/opencode/dist" >&2 + find dist -maxdepth 4 -type f >&2 + exit 1 + fi + echo "OPENSWARM_TUI_BINARY=$PWD/$binary" >> "$GITHUB_ENV" - name: Verify OpenSwarm agent roster through TUI env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: python3 scripts/smoke-run-mode.py --source local --check agents + run: python3 scripts/smoke-run-mode.py --source local --openswarm-tui-binary "$OPENSWARM_TUI_BINARY" --check agents - name: Verify live Agency Swarm Run-mode prompt env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - run: python3 scripts/smoke-run-mode.py --source local --check prompt + run: python3 scripts/smoke-run-mode.py --source local --openswarm-tui-binary "$OPENSWARM_TUI_BINARY" --check prompt diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml index 209d8d1..37cf669 100644 --- a/.github/workflows/test-mac.yml +++ b/.github/workflows/test-mac.yml @@ -7,6 +7,7 @@ on: - '.github/workflows/test-mac.yml' - 'bin/openswarm' - 'package.json' + - 'scripts/smoke-openswarm-launcher.js' jobs: test: @@ -16,13 +17,15 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20 + - name: Smoke-test launcher platform resolution + run: node scripts/smoke-openswarm-launcher.js - uses: actions/setup-python@v5 with: python-version: '3.12' - uses: actions/checkout@v4 with: repository: VRSEN/agentswarm-cli - ref: v1.4.34 + ref: v1.4.35 path: agentswarm-cli - uses: oven-sh/setup-bun@v2 with: @@ -42,7 +45,11 @@ jobs: AGENTSWARM_PRODUCT_MDNS_DOMAIN: openswarm.local AGENTSWARM_PRODUCT_STARTER_REPO: VRSEN/OpenSwarm AGENTSWARM_PRODUCT_STARTER_FOLDER: openswarm - AGENTSWARM_PRODUCT_ENTRY_FILES: swarm.py,agency.py + AGENTSWARM_PRODUCT_ENTRY_FILES: swarm.py + AGENTSWARM_PRODUCT_TUI_LOGO_LEFT: '[" "," ██████╗ ██████╗ ███████╗███╗ ██╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║","╚██████╔╝██║ ███████╗██║ ╚████║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝"]' + AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT: '["","███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║","███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║","╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_WORDMARK_LINES: '[""," ██████╗ ██████╗ ███████╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ╚════██║██║███╗██║██╔══██╗██╔══██╗██║╚██╔╝██║","╚██████╔╝██║ ███████╗██║ ╚████║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' + AGENTSWARM_PRODUCT_PYTHON_ENVIRONMENT: standalone AGENTSWARM_PRODUCT_VERSION: 1.0.1-rc.4 run: | bun install @@ -58,7 +65,6 @@ jobs: import os import pathlib import platform - import re import subprocess import sys import tempfile @@ -134,12 +140,12 @@ jobs: else: listing = None - failure_runs = [] + missing_runs = [] invalid_runs = [] success_runs = [] if binary is not None: - failure_env = {**env, "OPENSWARM_TUI_URL": f"{base_url}/missing/{binary_name}"} - failure_runs = [run_version(binary, root, failure_env) for _ in range(2)] + missing_env = {**env, "OPENSWARM_TUI_URL": f"{base_url}/missing/{binary_name}"} + missing_runs = [run_version(binary, root, missing_env) for _ in range(2)] invalid_binary = release_dir / binary_name invalid_binary.write_text("#!/bin/sh\nkill -9 $$\n", encoding="utf-8") @@ -174,8 +180,8 @@ jobs: "" if listing is None else listing.stderr, ] + [ - f"=== fallback run {idx} stdout ===\n{run.stdout}\n=== fallback run {idx} stderr ===\n{run.stderr}" - for idx, run in enumerate(failure_runs, start=1) + f"=== missing custom TUI run {idx} stdout ===\n{run.stdout}\n=== missing custom TUI run {idx} stderr ===\n{run.stderr}" + for idx, run in enumerate(missing_runs, start=1) ] + [ f"=== invalid run {idx} stdout ===\n{run.stdout}\n=== invalid run {idx} stderr ===\n{run.stderr}" @@ -196,9 +202,13 @@ jobs: print("FAILED: expected launcher not found after npm install") sys.exit(1) - for idx, run in enumerate(failure_runs, start=1): - if run.returncode != 0: - print(f"FAILED: fallback run {idx} exited with {run.returncode}") + for idx, run in enumerate(missing_runs, start=1): + if run.returncode == 0: + print(f"FAILED: missing custom TUI run {idx} unexpectedly succeeded") + sys.exit(1) + stderr = run.stderr.lower() + if "custom openswarm tui unavailable" not in stderr or "openswarm_tui_url" not in stderr: + print(f"FAILED: missing custom TUI run {idx} did not print actionable custom TUI unavailable message") sys.exit(1) for idx, run in enumerate(success_runs, start=1): @@ -207,8 +217,12 @@ jobs: sys.exit(1) for idx, run in enumerate(invalid_runs, start=1): - if run.returncode != 0: - print(f"FAILED: invalid run {idx} exited with {run.returncode}") + if run.returncode == 0: + print(f"FAILED: invalid run {idx} unexpectedly succeeded") + sys.exit(1) + stderr = run.stderr.lower() + if "custom openswarm tui unavailable" not in stderr or "openswarm_tui_url" not in stderr: + print(f"FAILED: invalid run {idx} did not print actionable custom TUI unavailable message") sys.exit(1) banned = [ @@ -223,30 +237,6 @@ jobs: print(f"FAILED: found unexpected output: {needle}") sys.exit(1) - fallback_version = next( - ( - line.strip() - for line in reversed(failure_runs[-1].stdout.splitlines()) - if line.strip() - ), - "", - ) - if not re.fullmatch(r"\d+\.\d+\.\d+", fallback_version): - print(f"FAILED: unexpected fallback version output: {fallback_version!r}") - sys.exit(1) - - invalid_version = next( - ( - line.strip() - for line in reversed(invalid_runs[-1].stdout.splitlines()) - if line.strip() - ), - "", - ) - if not re.fullmatch(r"\d+\.\d+\.\d+", invalid_version): - print(f"FAILED: unexpected invalid-binary fallback version output: {invalid_version!r}") - sys.exit(1) - custom_version = next( ( line.strip() diff --git a/bin/openswarm b/bin/openswarm index f971f9b..efff06f 100755 --- a/bin/openswarm +++ b/bin/openswarm @@ -11,6 +11,25 @@ const path = require('path') const scriptPath = fs.realpathSync(__filename) const packageDir = path.dirname(path.dirname(scriptPath)) const packageJson = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')) +const productTuiLogoLeft = [ + ' ', + ' ██████╗ ██████╗ ███████╗███╗ ██╗', + '██╔═══██╗██╔══██╗██╔════╝████╗ ██║', + '██║ ██║██████╔╝█████╗ ██╔██╗ ██║', + '██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║', + '╚██████╔╝██║ ███████╗██║ ╚████║', + ' ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝', +] +const productTuiLogoRight = [ + '', + '███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗', + '██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║', + '███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║', + '╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║', + '███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║', + '╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝', +] +const productWordmarkLines = productTuiLogoLeft.map((line, index) => `${line} ${productTuiLogoRight[index] ?? ''}`.trimEnd()) const downstreamEnv = { AGENTSWARM_PRODUCT_DISPLAY_NAME: 'OpenSwarm', AGENTSWARM_PRODUCT_COMMAND: 'openswarm', @@ -22,7 +41,12 @@ const downstreamEnv = { AGENTSWARM_PRODUCT_MDNS_DOMAIN: 'openswarm.local', AGENTSWARM_PRODUCT_STARTER_REPO: 'VRSEN/OpenSwarm', AGENTSWARM_PRODUCT_STARTER_FOLDER: 'openswarm', - AGENTSWARM_PRODUCT_ENTRY_FILES: 'swarm.py,agency.py', + AGENTSWARM_PRODUCT_ENTRY_FILES: 'swarm.py', + AGENTSWARM_PRODUCT_SKIP_POST_AUTH_MODEL_SELECTION: 'true', + AGENTSWARM_PRODUCT_TUI_LOGO_LEFT: JSON.stringify(productTuiLogoLeft), + AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT: JSON.stringify(productTuiLogoRight), + AGENTSWARM_PRODUCT_WORDMARK_LINES: JSON.stringify(productWordmarkLines), + AGENTSWARM_PRODUCT_PYTHON_ENVIRONMENT: 'standalone', AGENTSWARM_PRODUCT_VERSION: packageJson.version, } @@ -43,12 +67,46 @@ function resolveAgentswarm(startDir) { } } -function getBinaryName() { - return process.platform === 'win32' ? `agentswarm-windows-${os.arch() === 'arm64' ? 'arm64' : 'x64'}.exe` : `agentswarm-${process.platform === 'darwin' ? 'darwin' : 'linux'}-${os.arch() === 'arm64' ? 'arm64' : 'x64'}` +function getBinaryNames() { + const arch = os.arch() === 'arm64' ? 'arm64' : 'x64' + const platform = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'darwin' : 'linux' + const extension = process.platform === 'win32' ? '.exe' : '' + const names = [`agentswarm-${platform}-${arch}${extension}`] + if (arch === 'x64') { + names.push(`agentswarm-${platform}-${arch}-baseline${extension}`) + } + return names +} + +function hasExplicitTuiUrl() { + return Boolean(process.env.OPENSWARM_TUI_URL && process.env.OPENSWARM_TUI_URL.trim()) +} + +function isMuslLinux() { + if (process.platform !== 'linux') return false + + try { + if (fs.existsSync('/etc/alpine-release')) return true + } catch {} + + try { + const result = spawnSync('ldd', ['--version'], { + encoding: 'utf8', + timeout: 1500, + }) + const output = `${result.stdout || ''}\n${result.stderr || ''}`.toLowerCase() + return output.includes('musl') + } catch { + return false + } +} + +function shouldUseDependencyBinary() { + return isMuslLinux() && !hasExplicitTuiUrl() } function getDownloadUrl(binaryName) { - const explicit = process.env.OPENSWARM_TUI_URL && process.env.OPENSWARM_TUI_URL.trim() + const explicit = hasExplicitTuiUrl() ? process.env.OPENSWARM_TUI_URL.trim() : '' if (explicit) return explicit return `https://github.com/VRSEN/OpenSwarm/releases/download/v${packageJson.version}/${binaryName}` } @@ -152,30 +210,33 @@ async function download(url, destination) { } async function ensureCustomBinary() { - const binaryName = getBinaryName() - const binaryPath = path.join(packageDir, binaryName) - const existing = resolveCustomBinary(binaryPath) - if (existing) return existing + if (shouldUseDependencyBinary()) return null - fs.rmSync(binaryPath, { force: true }) + const binaryNames = getBinaryNames() + for (const binaryName of binaryNames) { + const existing = resolveCustomBinary(path.join(packageDir, binaryName)) + if (existing) return existing + } - const url = getDownloadUrl(binaryName) process.stdout.write('Downloading OpenSwarm TUI...\n') - try { - await download(url, binaryPath) - const resolved = resolveCustomBinary(binaryPath) - if (!resolved) { + const errors = [] + for (const binaryName of binaryNames) { + const binaryPath = path.join(packageDir, binaryName) + fs.rmSync(binaryPath, { force: true }) + try { + await download(getDownloadUrl(binaryName), binaryPath) + const resolved = resolveCustomBinary(binaryPath) + if (resolved) return resolved fs.rmSync(binaryPath, { force: true }) throw new Error('Downloaded custom TUI binary failed validation') + } catch (error) { + fs.rmSync(binaryPath, { force: true }) + errors.push(`${binaryName}: ${error instanceof Error ? error.message : String(error)}`) } - return resolved - } catch (error) { - fs.rmSync(binaryPath, { force: true }) - process.stderr.write( - `openswarm: custom OpenSwarm TUI unavailable (${error instanceof Error ? error.message : String(error)}); falling back to packaged AgentSwarm UI.\n`, - ) - return null } + throw new Error( + `custom OpenSwarm TUI unavailable (${errors.join('; ')}). Reinstall @vrsen/openswarm or set OPENSWARM_TUI_URL to a valid binary.`, + ) } const agentswarmBin = resolveAgentswarm(packageDir) diff --git a/onboard.py b/onboard.py index 3a838b8..f666886 100644 --- a/onboard.py +++ b/onboard.py @@ -26,6 +26,11 @@ _HAS_QUESTIONARY = True except ImportError: + class Choice: + def __init__(self, title: object, value: object | None = None) -> None: + self.title = title + self.value = title if value is None else value + _HAS_QUESTIONARY = False console = Console() diff --git a/package-lock.json b/package-lock.json index c449302..83c08da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@vrsen/agentswarm": "1.4.34", + "@vrsen/agentswarm": "1.4.35", "dom-to-pptx": "1.1.5", "patch-package": "^8.0.1", "playwright": "^1.59.1", @@ -496,21 +496,21 @@ } }, "node_modules/@vrsen/agentswarm": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm/-/agentswarm-1.4.34.tgz", - "integrity": "sha512-6cLzI/4I//Q2LeU90VG54zD6mqdGJ5OcdpXB7xIyrNgfJx6LTHjZ4qmvwKvaS+ZdMs43BZkr+7v9FSeVtvMWbw==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm/-/agentswarm-1.4.35.tgz", + "integrity": "sha512-4pf5nUTlOSaUSt/zo08IU/Ygt5YVegMW/2Wnx54WIwNjoeytggr150me8B4WTmK0RliapV7bd3sQDYVbKkkmEQ==", "license": "MIT", "dependencies": { - "agentswarm-cli": "1.4.34" + "agentswarm-cli": "1.4.35" }, "bin": { "agentswarm": "bin/agentswarm" } }, "node_modules/@vrsen/agentswarm-cli-darwin-arm64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-arm64/-/agentswarm-cli-darwin-arm64-1.4.34.tgz", - "integrity": "sha512-UHUB3yHS4RdFeywjtxfYbvpFhi0R9qqKsFEpIf4ADyVB4h+Ez2SGpRHVehR06ocuUOsUAbPOEvi9LHykvCok4Q==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-arm64/-/agentswarm-cli-darwin-arm64-1.4.35.tgz", + "integrity": "sha512-Zbu+2FUmWRr0qZ1lLArprGFeLB7cZjqkh4dFXlF9BRG3nX1DnOpTBZCBpjT7qE2Kz5ORbyIVUVhAp63w2t4Xdg==", "cpu": [ "arm64" ], @@ -520,9 +520,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-darwin-x64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-x64/-/agentswarm-cli-darwin-x64-1.4.34.tgz", - "integrity": "sha512-cck3Cz4arffd0QoyrMvMbMArwNLAKc4NJBHnO2SHLEjQusUDTKRZt+XcHg/ygYx6RplHaIYHyFkl47cXjF+uAQ==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-x64/-/agentswarm-cli-darwin-x64-1.4.35.tgz", + "integrity": "sha512-WPNQ6luTDP6+qsEYT/qu77M5uxnQ37/+uMv/hOIZlLRJ0eccaoiJxNiSTX0s/fr3xR6ZikF/1j3XxOvjYle/7w==", "cpu": [ "x64" ], @@ -532,9 +532,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-darwin-x64-baseline": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-x64-baseline/-/agentswarm-cli-darwin-x64-baseline-1.4.34.tgz", - "integrity": "sha512-wO+9EKyPPY3PnVePzfq262j8xGoeXO0PoUs7py/brFV9vtacQ2mb/saYwJtADTKQWuudjdMT02m/c46mky6P1Q==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-darwin-x64-baseline/-/agentswarm-cli-darwin-x64-baseline-1.4.35.tgz", + "integrity": "sha512-F5wOFJ1pV4BGBJAvF2KvSWMekZsqXixWGCfGWdIwnbEBw9Aj+knmFxRKZQF3bnzuwMCPvEIp9L3Dz3TxgpUutg==", "cpu": [ "x64" ], @@ -544,9 +544,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-arm64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-arm64/-/agentswarm-cli-linux-arm64-1.4.34.tgz", - "integrity": "sha512-WNIiz6CQlo7nBxtTAr6+J18bT9mWn4wh64oMVO0GEdrdB7TR6HxUOnf91N3Y1JOoqEhTSYcke7VnDddt5Oa0rg==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-arm64/-/agentswarm-cli-linux-arm64-1.4.35.tgz", + "integrity": "sha512-o6acJoznCG5Xknwh9eK/LZlCttkJCKwAXSSe+cvXewo11thjOP+7ho9rmzclyeIJkUU5xGPq4fhgYH77TnB6Jg==", "cpu": [ "arm64" ], @@ -556,9 +556,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-arm64-musl": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-arm64-musl/-/agentswarm-cli-linux-arm64-musl-1.4.34.tgz", - "integrity": "sha512-khcWx+nmFfxaKfLD5OuSUrEKe3HV7sFQURXFvcxTjxwFFMGoArRR/HZO+Oe2Y+Vip+cO6XAhTMQVXUHkhSOxQA==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-arm64-musl/-/agentswarm-cli-linux-arm64-musl-1.4.35.tgz", + "integrity": "sha512-WcvKaC1uniiOUWaiESgjuyf78Yj0Mw5SZwFYO6x0KnrSw8WNHsolouXv/VPyu4VrMKVFMAlksYn9RCPkpZz2fQ==", "cpu": [ "arm64" ], @@ -568,9 +568,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-x64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64/-/agentswarm-cli-linux-x64-1.4.34.tgz", - "integrity": "sha512-Tc3b/td5jDprlz7ByifPvea8cPKXW/4kXr0K8gSxbYor7qs1j31KEqguCSKUh5X4De0+cbfMJU5kf6rcbWRnSw==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64/-/agentswarm-cli-linux-x64-1.4.35.tgz", + "integrity": "sha512-reuVCj8wXHQSlA/Ve8sjLyftopgdpxGHf0ku6/SQ55uy+FFcTk8N+l+0+yrfgen7z4uV7m9gPU6KT9krAMY/RA==", "cpu": [ "x64" ], @@ -580,9 +580,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-x64-baseline": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-baseline/-/agentswarm-cli-linux-x64-baseline-1.4.34.tgz", - "integrity": "sha512-Vi7gyUPEvWfwzG6CUfWGPyeI7FEHZ6Xar6Tnax3fDu9XakeVjNLPJByfWAEepYONLlZ/2asSISgsp9PJpkwsYg==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-baseline/-/agentswarm-cli-linux-x64-baseline-1.4.35.tgz", + "integrity": "sha512-37R9TxyXFABmccXXdq6bVv8qKzeh1JgHxyWMi+uDEu7rH4mwFT/K6ZmREDpXJfVlwfmzMAUPJ4d0twZkuDXIKg==", "cpu": [ "x64" ], @@ -592,9 +592,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-x64-baseline-musl": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-baseline-musl/-/agentswarm-cli-linux-x64-baseline-musl-1.4.34.tgz", - "integrity": "sha512-UPUcGhA2Z+VTXsqk6grweVEoffB0wN8j9bT4I5vhHP1jMFJJU34NSRZG07Tfc/l7AgRyEXquC52YUHn8KA5z8g==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-baseline-musl/-/agentswarm-cli-linux-x64-baseline-musl-1.4.35.tgz", + "integrity": "sha512-bzROAlnVsF4Ww3N/C+Vt0jeUtN4oTcnahzRQyOMtpelspuGPhxW7uvfyHcmRC+hYZdFbP7628+2TMAiSp2ewUw==", "cpu": [ "x64" ], @@ -604,9 +604,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-linux-x64-musl": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-musl/-/agentswarm-cli-linux-x64-musl-1.4.34.tgz", - "integrity": "sha512-986/egNVEIhb7C4WWDJQ20CeDarUw6J//kSVR7Rwrd1Pu2sOdi9rS8VnwlUU5QqPGxk0Zh1fnFCyFxngA2bmbA==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-linux-x64-musl/-/agentswarm-cli-linux-x64-musl-1.4.35.tgz", + "integrity": "sha512-nGrT7C+gtoBWLRhpD7etoeem8nz6XIC8wq7AzSrHk4Ii9nQkyqZ3fcScP3haputtB4CKyb4OYHmPQGseSzAsZw==", "cpu": [ "x64" ], @@ -616,9 +616,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-windows-arm64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-arm64/-/agentswarm-cli-windows-arm64-1.4.34.tgz", - "integrity": "sha512-xDAiwz38fYEwK8tzqGBSEhesefu1xVrER/U/5jmIx7K4ldd3gPShLo7PWnJq7UOabtSA2b78yE7rfm/GhoqijA==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-arm64/-/agentswarm-cli-windows-arm64-1.4.35.tgz", + "integrity": "sha512-cUDPQ8uN7fxowsJwE5Q+bN0g+vNDR9+Oe8NR7atsII40D4ERDZ4IRJCrJO37P1yaIz041dprNsYZ9aM5BjT78g==", "cpu": [ "arm64" ], @@ -628,9 +628,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-windows-x64": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-x64/-/agentswarm-cli-windows-x64-1.4.34.tgz", - "integrity": "sha512-/Yuhtv8VetHfhPySPXwBTzyCxZeXdJ+W+t4klkmIJGDxPiZqkXc00vBzM4f+mgvwDNmh3F8y4sOUIQOcKXkz3Q==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-x64/-/agentswarm-cli-windows-x64-1.4.35.tgz", + "integrity": "sha512-+Ddu9W44GDTD4A/KHoSKtl7CYS8NfPdzCx13VNhlOZhBCaGzMDOAherHwj4KaXYV8thaENjQX5C8ckuxI1v0bA==", "cpu": [ "x64" ], @@ -640,9 +640,9 @@ ] }, "node_modules/@vrsen/agentswarm-cli-windows-x64-baseline": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-x64-baseline/-/agentswarm-cli-windows-x64-baseline-1.4.34.tgz", - "integrity": "sha512-x3CguvjJW8ascO13MnzDpZeT+qqs3NiUWnt+Z5mDOdITcYaCTc5CkncNCBYHRLgtLzAeMt/qcADwZRb1I5G4PQ==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/@vrsen/agentswarm-cli-windows-x64-baseline/-/agentswarm-cli-windows-x64-baseline-1.4.35.tgz", + "integrity": "sha512-Pi/UvIMAHf58fZ0Pm/ZfsUSrIsxCTm++v8nPDsGDmK1Q9JLSt2+ZYZVQiBE/S+AHFGDDJvhEQtMtKqj6qRwKEA==", "cpu": [ "x64" ], @@ -667,27 +667,27 @@ "license": "BSD-2-Clause" }, "node_modules/agentswarm-cli": { - "version": "1.4.34", - "resolved": "https://registry.npmjs.org/agentswarm-cli/-/agentswarm-cli-1.4.34.tgz", - "integrity": "sha512-wYLUFpsElbzz10gLB6KB6GNgd0j4SIkckiXnmy0onCWMoHoCZv8LA4VjX5iiOtT4HH1V7xP2DHJwMqAgSKKAHQ==", + "version": "1.4.35", + "resolved": "https://registry.npmjs.org/agentswarm-cli/-/agentswarm-cli-1.4.35.tgz", + "integrity": "sha512-QplH7PG/XgXBpOAykNiSMDNt7sH099anaVs9aST7lhr6Y9E958zClIDcdkJvGGCy8Nj/WUEmO/CsyMdnaes3ug==", "hasInstallScript": true, "license": "MIT", "bin": { "agentswarm": "bin/agentswarm" }, "optionalDependencies": { - "@vrsen/agentswarm-cli-darwin-arm64": "1.4.34", - "@vrsen/agentswarm-cli-darwin-x64": "1.4.34", - "@vrsen/agentswarm-cli-darwin-x64-baseline": "1.4.34", - "@vrsen/agentswarm-cli-linux-arm64": "1.4.34", - "@vrsen/agentswarm-cli-linux-arm64-musl": "1.4.34", - "@vrsen/agentswarm-cli-linux-x64": "1.4.34", - "@vrsen/agentswarm-cli-linux-x64-baseline": "1.4.34", - "@vrsen/agentswarm-cli-linux-x64-baseline-musl": "1.4.34", - "@vrsen/agentswarm-cli-linux-x64-musl": "1.4.34", - "@vrsen/agentswarm-cli-windows-arm64": "1.4.34", - "@vrsen/agentswarm-cli-windows-x64": "1.4.34", - "@vrsen/agentswarm-cli-windows-x64-baseline": "1.4.34" + "@vrsen/agentswarm-cli-darwin-arm64": "1.4.35", + "@vrsen/agentswarm-cli-darwin-x64": "1.4.35", + "@vrsen/agentswarm-cli-darwin-x64-baseline": "1.4.35", + "@vrsen/agentswarm-cli-linux-arm64": "1.4.35", + "@vrsen/agentswarm-cli-linux-arm64-musl": "1.4.35", + "@vrsen/agentswarm-cli-linux-x64": "1.4.35", + "@vrsen/agentswarm-cli-linux-x64-baseline": "1.4.35", + "@vrsen/agentswarm-cli-linux-x64-baseline-musl": "1.4.35", + "@vrsen/agentswarm-cli-linux-x64-musl": "1.4.35", + "@vrsen/agentswarm-cli-windows-arm64": "1.4.35", + "@vrsen/agentswarm-cli-windows-x64": "1.4.35", + "@vrsen/agentswarm-cli-windows-x64-baseline": "1.4.35" } }, "node_modules/ansi-styles": { diff --git a/package.json b/package.json index 1f07ed8..4bac16e 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,10 @@ "package-lock.json" ], "scripts": { - "postinstall": "node -e \"const fs=require('fs');const path=require('path');const cp=require('child_process');const pkg=__dirname;const patchTarget=path.join(pkg,'node_modules','dom-to-pptx');const patchCli=path.join(pkg,'node_modules','patch-package','index.js');if(fs.existsSync(patchTarget)&&fs.existsSync(patchCli)){cp.execFileSync(process.execPath,[patchCli],{cwd:pkg,stdio:'inherit'});}try{fs.chmodSync(path.join(pkg,'bin','openswarm'),0o755)}catch(e){}\"", - "smoke:run-mode": "python3 scripts/smoke-run-mode.py" + "postinstall": "node -e \"const fs=require('fs');const path=require('path');const cp=require('child_process');const pkg=__dirname;const patchTarget=path.join(pkg,'node_modules','dom-to-pptx');const patchCli=path.join(pkg,'node_modules','patch-package','index.js');if(fs.existsSync(patchTarget)&&fs.existsSync(patchCli)){cp.execFileSync(process.execPath,[patchCli],{cwd:pkg,stdio:'inherit'});}try{fs.chmodSync(path.join(pkg,'bin','openswarm'),0o755)}catch(e){}\"" }, "dependencies": { - "@vrsen/agentswarm": "1.4.34", + "@vrsen/agentswarm": "1.4.35", "dom-to-pptx": "1.1.5", "patch-package": "^8.0.1", "playwright": "^1.59.1", diff --git a/pyproject.toml b/pyproject.toml index efba6cb..e0f6e41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ openswarm = "run_utils:main" [tool.setuptools] -py-modules = ["agency", "swarm", "helpers", "config", "onboard", "server"] +py-modules = ["agency", "swarm", "helpers", "config", "onboard", "server", "run_utils"] [tool.setuptools.packages.find] where = ["."] diff --git a/run_utils.py b/run_utils.py index 76813e9..683fded 100644 --- a/run_utils.py +++ b/run_utils.py @@ -5,6 +5,84 @@ import tempfile from pathlib import Path +_PRODUCT_TUI_LOGO_LEFT = ( + '[" ",' + '" ██████╗ ██████╗ ███████╗███╗ ██╗",' + '"██╔═══██╗██╔══██╗██╔════╝████╗ ██║",' + '"██║ ██║██████╔╝█████╗ ██╔██╗ ██║",' + '"██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║",' + '"╚██████╔╝██║ ███████╗██║ ╚████║",' + '" ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝"]' +) +_PRODUCT_TUI_LOGO_RIGHT = ( + '["",' + '"███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗",' + '"██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║",' + '"███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║",' + '"╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║",' + '"███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║",' + '"╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' +) +_PRODUCT_WORDMARK_LINES = ( + '["",' + '" ██████╗ ██████╗ ███████╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗",' + '"██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║",' + '"██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║",' + '"██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ╚════██║██║███╗██║██╔══██╗██╔══██╗██║╚██╔╝██║",' + '"╚██████╔╝██║ ███████╗██║ ╚████║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║",' + '" ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]' +) + + +def _configure_product_env() -> None: + os.environ.setdefault("AGENTSWARM_PRODUCT_SKIP_POST_AUTH_MODEL_SELECTION", "true") + os.environ.setdefault("AGENTSWARM_PRODUCT_TUI_LOGO_LEFT", _PRODUCT_TUI_LOGO_LEFT) + os.environ.setdefault("AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT", _PRODUCT_TUI_LOGO_RIGHT) + os.environ.setdefault("AGENTSWARM_PRODUCT_WORDMARK_LINES", _PRODUCT_WORDMARK_LINES) + + +def _preload_agentswarm_bin(repo: Path | None = None) -> None: + # Bootstrap may install python-dotenv, so preserve this one override with stdlib. + if "AGENTSWARM_BIN" in os.environ: + return + + roots = [repo] if repo else [Path.cwd(), Path(__file__).resolve().parent] + seen: set[Path] = set() + + for root in roots: + if root is None: + continue + path = root.resolve() / ".env" + if path in seen: + continue + seen.add(path) + try: + lines = path.read_text(encoding="utf-8").splitlines() + except OSError: + continue + + for line in lines: + value = line.strip() + if not value or value.startswith("#"): + continue + if value.startswith("export "): + value = value.removeprefix("export ").lstrip() + + key, sep, raw = value.partition("=") + if sep != "=" or key.strip() != "AGENTSWARM_BIN": + continue + + raw = raw.strip() + if raw[:1] in {"'", '"'}: + quote = raw[0] + end = raw.find(quote, 1) + raw = raw[1:end] if end != -1 else raw[1:] + else: + raw = raw.split(" #", 1)[0].strip() + os.environ["AGENTSWARM_BIN"] = raw + return + + def _resolve_bin_name() -> str: """Return the platform+arch-specific TUI binary filename.""" import platform @@ -17,6 +95,62 @@ def _resolve_bin_name() -> str: return f"agentswarm-linux-{arch}" +def _resolve_bin_names() -> list[str]: + name = _resolve_bin_name() + names = [name] + stem, suffix = (name[:-4], ".exe") if name.endswith(".exe") else (name, "") + if stem.endswith("-x64"): + names.append(f"{stem}-baseline{suffix}") + return names + + +def _is_tui_binary_runnable(path: Path) -> bool: + try: + stat = path.stat() + if not stat.st_size: + return False + if sys.platform != "win32" and not os.access(path, os.X_OK): + path.chmod(0o755) + result = subprocess.run( + [str(path), "--version"], + env={**os.environ, "AGENTSWARM_LAUNCHER": "0"}, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=15, + ) + except Exception: + return False + return result.returncode == 0 + + +def _download_tui_binary(repo: Path, name: str) -> Path | None: + import urllib.request + + path = repo / name + url = f"https://github.com/VRSEN/OpenSwarm/releases/latest/download/{name}" + print("Downloading OpenSwarm TUI, please wait…\n") + try: + urllib.request.urlretrieve(url, str(path)) + if sys.platform != "win32": + path.chmod(0o755) + print("\nDone.\n") + except Exception: + path.unlink(missing_ok=True) + return None + return path + + +def _resolve_tui_binary(repo: Path, download: bool) -> Path | None: + for name in _resolve_bin_names(): + path = repo / name + if not path.exists() and download: + path = _download_tui_binary(repo, name) or path + if path.exists() and _is_tui_binary_runnable(path): + return path + return None + + def _ensure_node_playwright_browsers(repo: Path) -> None: """Install Node Playwright browsers where the HTML-to-PPTX runner looks for them.""" cli = repo / "node_modules" / "playwright" / "cli.js" @@ -44,8 +178,8 @@ def _uv_env() -> dict[str, str]: # ── Bootstrap: create venv + install deps automatically on first run ───────── # Only stdlib imports above. _bootstrap() is called explicitly — either from -# swarm.py (via `from run import _bootstrap; _bootstrap()`) or from the -# __main__ guard below — never at module level, so `from run import _bootstrap` +# swarm.py (via `from run_utils import _bootstrap; _bootstrap()`) or from the +# __main__ guard below — never at module level, so `from run_utils import _bootstrap` # is safe to call from outside the venv. def _bootstrap() -> None: _repo = Path(__file__).resolve().parent @@ -141,19 +275,12 @@ def _bootstrap() -> None: pass # Download the OpenSwarm TUI binary from GitHub Releases if missing. - _bin_name = _resolve_bin_name() - _bin_path = _repo / _bin_name - if not _bin_path.exists(): - import urllib.request - _bin_url = f"https://github.com/VRSEN/OpenSwarm/releases/latest/download/{_bin_name}" - print("Downloading OpenSwarm TUI, please wait…\n") - try: - urllib.request.urlretrieve(_bin_url, str(_bin_path)) - if sys.platform != "win32": - _bin_path.chmod(0o755) - print("\nDone.\n") - except Exception: - print("Warning: Could not download OpenSwarm TUI. The terminal UI will use the default.\n") + if not os.getenv("AGENTSWARM_BIN"): + _bin_path = _resolve_tui_binary(_repo, download=True) + if _bin_path: + os.environ["AGENTSWARM_BIN"] = str(_bin_path) + else: + print("Warning: Could not download a runnable OpenSwarm TUI. The terminal UI will use the default.\n") # ───────────────────────────────────────────────────────────────────────────── @@ -232,16 +359,20 @@ def _configure_demo_console() -> None: def main() -> None: + _preload_agentswarm_bin() + _bootstrap() + from dotenv import load_dotenv load_dotenv() os.environ.setdefault("PYTHONUTF8", "1") os.environ.setdefault("PYTHONIOENCODING", "utf-8") + _configure_product_env() if not os.getenv("AGENTSWARM_BIN"): _repo = Path(__file__).resolve().parent - local_exe = _repo / _resolve_bin_name() - if local_exe.exists(): + local_exe = _resolve_tui_binary(_repo, download=True) + if local_exe: os.environ["AGENTSWARM_BIN"] = str(local_exe) # Disable OpenAI Agents SDK tracing for terminal demo runs. @@ -303,5 +434,4 @@ def main() -> None: if __name__ == "__main__": - _bootstrap() main() diff --git a/scripts/smoke-bootstrap-onboard.py b/scripts/smoke-bootstrap-onboard.py new file mode 100644 index 0000000..6480b18 --- /dev/null +++ b/scripts/smoke-bootstrap-onboard.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Focused smoke checks for OpenSwarm import bootstrap and onboarding writes.""" + +from __future__ import annotations + +import importlib.util +import io +import os +import sys +import tempfile +import types +from collections.abc import Iterator +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +@contextmanager +def swapped_modules(replacements: dict[str, types.ModuleType]) -> Iterator[None]: + marker = object() + previous = {name: sys.modules.get(name, marker) for name in replacements} + sys.modules.update(replacements) + try: + yield + finally: + for name, module in previous.items(): + if module is marker: + sys.modules.pop(name, None) + else: + sys.modules[name] = module + + +def module(name: str, **attrs: object) -> types.ModuleType: + mod = types.ModuleType(name) + for key, value in attrs.items(): + setattr(mod, key, value) + return mod + + +def smoke_swarm_import_skips_bootstrap() -> None: + order: list[str] = [] + + patches = module("patches", __path__=[]) + replacements = { + "run_utils": module( + "run_utils", + _bootstrap=lambda: order.append("bootstrap"), + _preload_agentswarm_bin=lambda: order.append("preload"), + ), + "dotenv": module("dotenv", load_dotenv=lambda: order.append("dotenv")), + "agents": module( + "agents", + set_tracing_disabled=lambda _value: order.append("agents"), + set_tracing_export_api_key=lambda _value: order.append("agents"), + ), + "patches": patches, + "patches.patch_agency_swarm_dual_comms": module( + "patches.patch_agency_swarm_dual_comms", + apply_dual_comms_patch=lambda: order.append("patch"), + ), + "patches.patch_file_attachment_refs": module( + "patches.patch_file_attachment_refs", + apply_file_attachment_reference_patch=lambda: order.append("patch"), + ), + "patches.patch_ipython_interpreter_composio": module( + "patches.patch_ipython_interpreter_composio", + apply_ipython_composio_context_patch=lambda: order.append("patch"), + ), + "patches.patch_utf8_file_reads": module( + "patches.patch_utf8_file_reads", + apply_utf8_file_read_patch=lambda: order.append("patch"), + ), + } + + spec = importlib.util.spec_from_file_location("swarm_bootstrap_smoke", ROOT / "swarm.py") + if not spec or not spec.loader: + raise RuntimeError("could not load swarm.py import spec") + + old_key = os.environ.pop("OPENAI_API_KEY", None) + try: + with swapped_modules(replacements): + swarm = importlib.util.module_from_spec(spec) + spec.loader.exec_module(swarm) + finally: + if old_key is not None: + os.environ["OPENAI_API_KEY"] = old_key + sys.modules.pop("swarm_bootstrap_smoke", None) + + if "bootstrap" in order: + raise RuntimeError(f"swarm.py ran bootstrap during import: {order}") + if not order or order[0] != "dotenv": + raise RuntimeError(f"swarm.py did not configure runtime during import: {order}") + + +def smoke_onboard_env_writes() -> None: + sys.path.insert(0, str(ROOT)) + try: + import onboard + from rich.console import Console + finally: + sys.path.pop(0) + + provider = next(item for item in onboard.PROVIDERS if item["name"] == "OpenAI") + secrets = iter( + [ + "sk-test-openai", + "search-test-key", + "composio-test-key", + "composio-test-user", + ] + ) + + with tempfile.TemporaryDirectory(prefix="openswarm-onboard-smoke-") as tmp: + env = Path(tmp) / ".env" + sink = io.StringIO() + with ( + patch.object(onboard, "ENV_PATH", env), + patch.object(onboard, "console", Console(file=sink, force_terminal=False)), + patch.object(onboard, "_ask_select", lambda _message, _choices: provider), + patch.object( + onboard, + "_ask_checkbox", + lambda _message, _choices: ["search", "composio"], + ), + patch.object(onboard, "_ask_secret", lambda _label, _url: next(secrets)), + patch.object(onboard, "_ask_confirm", lambda _message, default=True: default), + ): + onboard.run_onboarding() + + values = onboard.dotenv_values(str(env)) + + expected = { + "OPENAI_API_KEY": "sk-test-openai", + "DEFAULT_MODEL": provider["default_model"], + "SEARCH_API_KEY": "search-test-key", + "COMPOSIO_API_KEY": "composio-test-key", + "COMPOSIO_USER_ID": "composio-test-user", + } + missing = {key: value for key, value in expected.items() if values.get(key) != value} + if missing: + raise RuntimeError(f"onboarding did not write expected .env values: {missing}") + + +def main() -> int: + smoke_swarm_import_skips_bootstrap() + smoke_onboard_env_writes() + print("OpenSwarm import bootstrap and onboarding smoke passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/smoke-openswarm-launcher.js b/scripts/smoke-openswarm-launcher.js new file mode 100644 index 0000000..5022c8c --- /dev/null +++ b/scripts/smoke-openswarm-launcher.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node +'use strict' + +const assert = require('assert') +const EventEmitter = require('events') +const fs = require('fs') +const path = require('path') +const vm = require('vm') + +const root = path.dirname(__dirname) +const launcher = path.join(root, 'bin', 'openswarm') + +function createClient(requests) { + return { + get(url, _opts, callback) { + requests.push(url) + const req = new EventEmitter() + req.setTimeout = () => req + req.destroy = (error) => req.emit('error', error) + + process.nextTick(() => { + const res = new EventEmitter() + res.statusCode = 404 + res.headers = {} + res.resume = () => {} + callback(res) + }) + + return req + }, + } +} + +function load(opts) { + const requests = [] + const child = { + spawnSync(command) { + if (command === 'ldd') { + return { + status: 0, + stdout: opts.ldd || '', + stderr: '', + } + } + return { + status: 1, + stdout: '', + stderr: '', + } + }, + } + const files = { + ...fs, + existsSync(target) { + if (target === '/etc/alpine-release') return Boolean(opts.alpine) + return fs.existsSync(target) + }, + } + const proc = { + ...process, + arch: () => opts.arch, + env: { + ...process.env, + ...opts.env, + }, + platform: opts.platform, + stdout: { + write() {}, + }, + } + const modules = { + assert, + events: EventEmitter, + fs: files, + http: createClient(requests), + https: createClient(requests), + os: { + arch: () => opts.arch, + }, + path, + 'child_process': child, + } + const context = { + Buffer, + Error, + JSON, + Promise, + URL, + console, + module: { exports: {} }, + process: proc, + require(name) { + if (modules[name]) return modules[name] + return require(name) + }, + __filename: launcher, + } + context.exports = context.module.exports + + const source = fs.readFileSync(launcher, 'utf8') + const start = source.startsWith('#!') ? source.indexOf('\n') + 1 : 0 + const startup = source.indexOf('\nconst agentswarmBin = resolveAgentswarm(packageDir)') + assert.notEqual(startup, -1, 'launcher startup block not found') + const script = new vm.Script( + `${source.slice(start, startup)} +module.exports = { getBinaryNames, isMuslLinux, shouldUseDependencyBinary, ensureCustomBinary }`, + { filename: launcher }, + ) + script.runInNewContext(context) + + return { + api: context.module.exports, + requests, + } +} + +async function main() { + const musl = load({ + platform: 'linux', + arch: 'x64', + ldd: 'musl libc (x86_64)', + }) + assert.equal(musl.api.isMuslLinux(), true) + assert.equal(musl.api.shouldUseDependencyBinary(), true) + assert.equal(await musl.api.ensureCustomBinary(), null) + assert.deepEqual(musl.requests, []) + + const explicit = load({ + platform: 'linux', + arch: 'x64', + env: { + OPENSWARM_TUI_URL: 'https://example.test/openswarm', + }, + ldd: 'musl libc (x86_64)', + }) + assert.equal(explicit.api.shouldUseDependencyBinary(), false) + assert.deepEqual(explicit.api.getBinaryNames(), ['agentswarm-linux-x64', 'agentswarm-linux-x64-baseline']) + + const glibc = load({ + platform: 'linux', + arch: 'x64', + ldd: 'ldd (GNU libc)', + }) + assert.equal(glibc.api.isMuslLinux(), false) + assert.equal(glibc.api.shouldUseDependencyBinary(), false) + await assert.rejects(() => glibc.api.ensureCustomBinary(), /custom OpenSwarm TUI unavailable/) + assert.ok(glibc.requests.some((url) => url.includes('agentswarm-linux-x64'))) + + console.log('openswarm launcher smoke passed') +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/scripts/smoke-run-mode.py b/scripts/smoke-run-mode.py index ba9f6cc..ecbb28f 100644 --- a/scripts/smoke-run-mode.py +++ b/scripts/smoke-run-mode.py @@ -7,6 +7,7 @@ import json import os import pathlib +import platform import pty import re import select @@ -23,6 +24,9 @@ DEFAULT_PROMPT = "Reply exactly OPEN_SWARM_RUN_SMOKE_OK." DEFAULT_EXPECT = "OPEN_SWARM_RUN_SMOKE_OK" +HANDOFF_PROMPT = "handoff to the data analyst" +HANDOFF_FOLLOWUP_PROMPT = "Reply exactly OPEN_SWARM_HANDOFF_OK." +HANDOFF_EXPECT = "OPEN_SWARM_HANDOFF_OK" EXPECTED_AGENCY_NAME = "OpenSwarm" EXPECTED_ENTRY_AGENT = "Orchestrator" EXPECTED_SPECIALIST_AGENTS = [ @@ -34,6 +38,7 @@ "Video Agent", "Image Agent", ] +EXPECTED_AGENT_COUNT = 1 + len(EXPECTED_SPECIALIST_AGENTS) def run(cmd: Sequence[str], *, cwd: pathlib.Path, env: dict[str, str]) -> subprocess.CompletedProcess[str]: @@ -48,7 +53,85 @@ def latest_pack_stdout_line(stdout: str) -> str: raise RuntimeError("npm pack did not print a tarball name") -def install_package(repo: pathlib.Path, root: pathlib.Path, source: str, npm_spec: str, env: dict[str, str]) -> pathlib.Path: +def resolve_local_package(value: str) -> pathlib.Path: + package = pathlib.Path(value).expanduser() + if not package.is_absolute(): + package = pathlib.Path.cwd() / package + package = package.resolve() + if not package.exists(): + raise RuntimeError(f"local AgentSwarm package does not exist at {package}") + return package + + +def resolve_models_fixture(package: pathlib.Path | None) -> pathlib.Path | None: + if not package or not package.is_dir(): + return None + fixture = package / "test" / "tool" / "fixtures" / "models-api.json" + return fixture if fixture.exists() else None + + +def resolve_local_binary(value: str) -> pathlib.Path: + binary = pathlib.Path(value).expanduser() + if not binary.is_absolute(): + binary = pathlib.Path.cwd() / binary + binary = binary.resolve() + if not binary.exists(): + raise RuntimeError(f"local OpenSwarm TUI binary does not exist at {binary}") + if not binary.is_file(): + raise RuntimeError(f"local OpenSwarm TUI binary is not a file at {binary}") + return binary + + +def platform_asset_name() -> str: + machine = platform.machine().lower() + if machine in {"arm64", "aarch64"}: + arch = "arm64" + elif machine in {"x86_64", "amd64"}: + arch = "x64" + else: + arch = None + + if sys.platform == "darwin" and arch: + return f"agentswarm-darwin-{arch}" + if sys.platform.startswith("linux") and arch: + return f"agentswarm-linux-{arch}" + if sys.platform == "win32" and arch: + return f"agentswarm-windows-{arch}.exe" + raise RuntimeError( + f"unsupported OpenSwarm TUI asset platform: {sys.platform} {platform.machine()}" + ) + + +def install_openswarm_tui_binary(package_dir: pathlib.Path, binary: pathlib.Path) -> pathlib.Path: + target = package_dir / platform_asset_name() + shutil.copy2(binary, target) + if os.name != "nt": + target.chmod(target.stat().st_mode | 0o111) + return target + + +def create_local_openswarm_project(package_dir: pathlib.Path, root: pathlib.Path) -> pathlib.Path: + target = root / "openswarm" + ignore = shutil.ignore_patterns( + ".git", + ".venv", + "__pycache__", + "node_modules", + "agentswarm-*", + "*.tgz", + ) + shutil.copytree(package_dir, target, ignore=ignore) + return target + + +def install_package( + repo: pathlib.Path, + root: pathlib.Path, + source: str, + npm_spec: str, + agentswarm_path: pathlib.Path | None, + env: dict[str, str], +) -> pathlib.Path: run(["npm", "init", "-y"], cwd=root, env=env) if source == "local": packed = run(["npm", "pack"], cwd=repo, env=env) @@ -60,6 +143,9 @@ def install_package(repo: pathlib.Path, root: pathlib.Path, source: str, npm_spe else: run(["npm", "install", npm_spec], cwd=root, env=env) + if agentswarm_path: + run(["npm", "install", str(agentswarm_path)], cwd=root, env=env) + launcher = root / "node_modules" / ".bin" / "openswarm" if not launcher.exists(): raise RuntimeError(f"openswarm launcher was not installed at {launcher}") @@ -136,6 +222,7 @@ def set_window_size(fd: int, rows: int = 45, columns: int = 180) -> None: def run_tui_smoke( launcher: pathlib.Path, package_dir: pathlib.Path, + launcher_cwd: pathlib.Path, root: pathlib.Path, env: dict[str, str], check: str, @@ -147,7 +234,7 @@ def run_tui_smoke( set_window_size(slave_fd) process = subprocess.Popen( [str(launcher)], - cwd=package_dir, + cwd=launcher_cwd, env=env, stdin=slave_fd, stdout=slave_fd, @@ -162,10 +249,29 @@ def run_tui_smoke( sent_agents_command = False verified_agents = False closed_agents_at: float | None = None + sent_models_slash = False + saw_models_slash = False + cleared_models_probe = False + sent_models_direct = False + saw_models_direct = False + selected_models_command = False + models_slash_at: float | None = None + models_clear_at: float | None = None + models_direct_at: float | None = None + models_slash_start = 0 + models_direct_start = 0 + models_picker_start = 0 + verified_models = False + closed_models_at: float | None = None sent_prompt = False saw_expected = False + sent_handoff_prompt = False + saw_handoff = False + handoff_seen_at: float | None = None + sent_handoff_followup = False deadline = time.monotonic() + timeout expected_compact = compact(expected) + handoff_expected_compact = compact(HANDOFF_EXPECT) expected_agent_terms = [EXPECTED_AGENCY_NAME, EXPECTED_ENTRY_AGENT, *EXPECTED_SPECIALIST_AGENTS] expected_agent_compact = [compact(term) for term in expected_agent_terms] @@ -182,6 +288,10 @@ def run_tui_smoke( plain = strip_ansi(raw) compact_plain = compact(plain) + lower_compact = compact_plain.lower() + + if "failedtostartresponsestream" in lower_compact or "cannotreachagency-swarmbackend" in lower_compact: + raise RuntimeError("OpenSwarm backend became unreachable during smoke test") if not sent_confirm and "Createalocal`.venv`inthisproject?" in compact_plain: write(master_fd, "\r") @@ -206,12 +316,100 @@ def run_tui_smoke( verified_agents = True closed_agents_at = closed_agents_at or time.monotonic() + if check == "handoff" and run_mode_ready and not sent_handoff_prompt: + write(master_fd, HANDOFF_PROMPT + "\r") + sent_handoff_prompt = True + + if sent_handoff_prompt and not saw_handoff: + if "transfer_to_Data_Analyst" in plain or "DataAnalyst" in compact_plain: + saw_handoff = True + handoff_seen_at = time.monotonic() + + if ( + saw_handoff + and not sent_handoff_followup + and handoff_seen_at is not None + and time.monotonic() - handoff_seen_at > 0.5 + ): + write(master_fd, HANDOFF_FOLLOWUP_PROMPT + "\r") + sent_handoff_followup = True + + if check == "handoff" and (HANDOFF_EXPECT in plain or handoff_expected_compact in compact_plain): + return plain + + agents_ready = check not in {"agents", "all"} or verified_agents + if ( + check in {"models", "all"} + and agents_ready + and not sent_models_slash + and run_mode_ready + and (closed_agents_at is None or time.monotonic() - closed_agents_at > 0.5) + ): + models_slash_start = len(plain) + write(master_fd, "/") + sent_models_slash = True + models_slash_at = time.monotonic() + + if sent_models_slash and not selected_models_command: + models_slash = compact(plain[models_slash_start:]).lower() + if not saw_models_slash and "/models" in models_slash: + saw_models_slash = True + if ( + saw_models_slash + and not cleared_models_probe + and models_slash_at is not None + and time.monotonic() - models_slash_at > 0.5 + ): + write(master_fd, "\x7f") + cleared_models_probe = True + models_clear_at = time.monotonic() + elif ( + cleared_models_probe + and not sent_models_direct + and models_clear_at is not None + and time.monotonic() - models_clear_at > 0.2 + ): + models_direct_start = len(plain) + write(master_fd, "/models") + sent_models_direct = True + models_direct_at = time.monotonic() + elif sent_models_direct and not saw_models_direct: + models_direct = compact(plain[models_direct_start:]).lower() + if "/models" in models_direct and "switchmodel" in models_direct: + saw_models_direct = True + elif ( + saw_models_direct + and models_direct_at is not None + and time.monotonic() - models_direct_at > 0.5 + ): + models_picker_start = len(plain) + write(master_fd, "\r") + selected_models_command = True + + if selected_models_command and not verified_models: + models_picker = compact(plain[models_picker_start:]).lower() + if "selectmodel" in models_picker and ( + "agencyswarmdefault" in models_picker or "manageproviderauth" in models_picker + ): + verified_models = True + write(master_fd, "\x1b") + closed_models_at = time.monotonic() + if check == "models": + return plain + + if check == "prompt": + verified_models = True + closed_models_at = closed_models_at or closed_agents_at or time.monotonic() + + models_ready = check not in {"models", "all"} or verified_models + closed_picker_at = closed_models_at or closed_agents_at if ( check in {"prompt", "all"} and verified_agents + and models_ready and not sent_prompt - and closed_agents_at is not None - and time.monotonic() - closed_agents_at > 0.5 + and closed_picker_at is not None + and time.monotonic() - closed_picker_at > 0.5 ): write(master_fd, prompt + "\r") sent_prompt = True @@ -229,7 +427,13 @@ def run_tui_smoke( raise RuntimeError( "OpenSwarm Run-mode smoke test did not reach the expected response. " f"sent_confirm={sent_confirm} sent_agents_command={sent_agents_command} " - f"verified_agents={verified_agents} sent_prompt={sent_prompt} saw_expected={saw_expected} log={log_path}" + f"verified_agents={verified_agents} sent_models_slash={sent_models_slash} " + f"saw_models_slash={saw_models_slash} cleared_models_probe={cleared_models_probe} " + f"sent_models_direct={sent_models_direct} saw_models_direct={saw_models_direct} " + f"selected_models_command={selected_models_command} " + f"verified_models={verified_models} sent_prompt={sent_prompt} saw_expected={saw_expected} " + f"sent_handoff_prompt={sent_handoff_prompt} saw_handoff={saw_handoff} " + f"sent_handoff_followup={sent_handoff_followup} log={log_path}" ) @@ -237,7 +441,15 @@ def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--source", choices=["local", "npm"], default="local") parser.add_argument("--npm-spec") - parser.add_argument("--check", choices=["all", "agents", "prompt"], default="all") + parser.add_argument( + "--agentswarm-package", + help="Local @vrsen/agentswarm tarball or package directory to install after OpenSwarm for smoke proof.", + ) + parser.add_argument( + "--openswarm-tui-binary", + help="Local OpenSwarm-branded AgentSwarm TUI binary to copy into the installed @vrsen/openswarm package.", + ) + parser.add_argument("--check", choices=["all", "agents", "models", "prompt", "handoff"], default="all") parser.add_argument("--prompt", default=DEFAULT_PROMPT) parser.add_argument("--expect", default=DEFAULT_EXPECT) parser.add_argument("--timeout", type=int, default=1200) @@ -247,11 +459,14 @@ def main() -> int: repo = pathlib.Path(__file__).resolve().parents[1] package_json = json.loads((repo / "package.json").read_text(encoding="utf-8")) npm_spec = args.npm_spec or f"{package_json['name']}@{package_json['version']}" + agentswarm_path = resolve_local_package(args.agentswarm_package) if args.agentswarm_package else None + openswarm_tui_binary = resolve_local_binary(args.openswarm_tui_binary) if args.openswarm_tui_binary else None + models_fixture = resolve_models_fixture(agentswarm_path) api_key = os.environ.get("OPENAI_API_KEY") - if not api_key and args.check == "prompt" and os.environ.get("GITHUB_ACTIONS") == "true": + if not api_key and args.check in {"prompt", "handoff"} and os.environ.get("GITHUB_ACTIONS") == "true": print("Skipped OpenSwarm live prompt smoke because OPENAI_API_KEY is not configured") return 0 - if not api_key and args.check in {"all", "prompt"}: + if not api_key and args.check in {"all", "prompt", "handoff"}: raise RuntimeError("OPENAI_API_KEY is required for the live prompt smoke test") auth_key = api_key or "dummy-openai-key-for-agent-roster-smoke" root = pathlib.Path(tempfile.mkdtemp(prefix="openswarm-run-mode-smoke-")) @@ -260,25 +475,51 @@ def main() -> int: { "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8", + "CI": "1", "TERM": "xterm-256color", "OPENCODE_AUTH_CONTENT": json.dumps({"openai": {"type": "api", "key": auth_key}}), "XDG_DATA_HOME": str(root / "xdg-data"), "XDG_CONFIG_HOME": str(root / "xdg-config"), "XDG_CACHE_HOME": str(root / "xdg-cache"), "XDG_STATE_HOME": str(root / "xdg-state"), + "OPENCODE_DISABLE_AUTOUPDATE": "true", + "OPENCODE_DISABLE_MODELS_FETCH": "true", } ) + if models_fixture: + env["OPENCODE_MODELS_PATH"] = str(models_fixture) try: - launcher = install_package(repo, root, args.source, npm_spec, env) + launcher = install_package(repo, root, args.source, npm_spec, agentswarm_path, env) package_dir = root / "node_modules" / "@vrsen" / "openswarm" - plain = run_tui_smoke(launcher, package_dir, root, env, args.check, args.prompt, args.expect, args.timeout) + if openswarm_tui_binary: + install_openswarm_tui_binary(package_dir, openswarm_tui_binary) + generic_dir = root / "my-agency" + generic_dir.mkdir() + (generic_dir / "agency.py").write_text( + "\n".join( + [ + "from agency_swarm import Agency", + "", + "def create_agency(load_threads_callback=None):", + " return Agency(name='Generic Agency')", + "", + ] + ), + encoding="utf-8", + ) + create_local_openswarm_project(package_dir, generic_dir) + plain = run_tui_smoke(launcher, package_dir, generic_dir, root, env, args.check, args.prompt, args.expect, args.timeout) if "Agency Swarm Default" not in plain: raise RuntimeError("Smoke response was seen, but Agency Swarm Run mode was not detected") if args.check in {"agents", "all"}: - print(f"OpenSwarm /agents smoke passed with {len(EXPECTED_SPECIALIST_AGENTS)} specialists visible") + print(f"OpenSwarm /agents smoke passed with {EXPECTED_AGENT_COUNT} agents visible") + if args.check in {"models", "all"}: + print("OpenSwarm /models smoke passed") if args.check in {"prompt", "all"}: print("OpenSwarm live prompt smoke passed") + if args.check == "handoff": + print("OpenSwarm handoff smoke passed") print(f"OpenSwarm smoke root package: {package_dir}") return 0 finally: diff --git a/swarm.py b/swarm.py index 7ddfde4..4a026dd 100644 --- a/swarm.py +++ b/swarm.py @@ -1,26 +1,50 @@ import os -from dotenv import load_dotenv -from agents import set_tracing_disabled, set_tracing_export_api_key -from patches.patch_agency_swarm_dual_comms import apply_dual_comms_patch -from patches.patch_file_attachment_refs import apply_file_attachment_reference_patch -from patches.patch_ipython_interpreter_composio import apply_ipython_composio_context_patch -from patches.patch_utf8_file_reads import apply_utf8_file_read_patch -load_dotenv() +from run_utils import _bootstrap, _preload_agentswarm_bin -apply_utf8_file_read_patch() -apply_dual_comms_patch() -apply_file_attachment_reference_patch() -apply_ipython_composio_context_patch() +_RUNTIME_CONFIGURED = False -_tracing_key = os.getenv("OPENAI_API_KEY") -if _tracing_key: - set_tracing_export_api_key(_tracing_key) -else: - set_tracing_disabled(True) + +def _configure_runtime() -> None: + global _RUNTIME_CONFIGURED + if _RUNTIME_CONFIGURED: + return + + from dotenv import load_dotenv + from agents import set_tracing_disabled, set_tracing_export_api_key + from patches.patch_agency_swarm_dual_comms import apply_dual_comms_patch + from patches.patch_file_attachment_refs import apply_file_attachment_reference_patch + from patches.patch_ipython_interpreter_composio import ( + apply_ipython_composio_context_patch, + ) + from patches.patch_utf8_file_reads import apply_utf8_file_read_patch + + load_dotenv() + + apply_utf8_file_read_patch() + apply_dual_comms_patch() + apply_file_attachment_reference_patch() + apply_ipython_composio_context_patch() + + _tracing_key = os.getenv("OPENAI_API_KEY") + if _tracing_key: + set_tracing_export_api_key(_tracing_key) + else: + set_tracing_disabled(True) + + _RUNTIME_CONFIGURED = True + + +if __name__ == "__main__": + _preload_agentswarm_bin() + _bootstrap() + +_configure_runtime() def create_agency(load_threads_callback=None): + _configure_runtime() + from agency_swarm import Agency from agency_swarm.tools import Handoff, SendMessage @@ -76,6 +100,11 @@ def create_agency(load_threads_callback=None): return agency -if __name__ == "__main__": + +def _main() -> None: agency = create_agency() agency.tui(show_reasoning=True, reload=False) + + +if __name__ == "__main__": + _main()