diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 9cb456745..448108d05 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -8,7 +8,6 @@ on: env: UV_VERSION: "0.8.0" - PYTHON_VERSION: "3.13" NOX_DEFAULT_VENV_BACKEND: "uv" jobs: @@ -37,25 +36,16 @@ jobs: - name: Run lint via nox run: | - mkdir -p build - nox -s lint 2>&1 | tee build/nox-lint-output.log + nox -s lint env: NOX_DEFAULT_VENV_BACKEND: uv - name: Run format check via nox run: | - nox -s format -- --check 2>&1 | tee -a build/nox-lint-output.log + nox -s format -- --check env: NOX_DEFAULT_VENV_BACKEND: uv - - name: Upload lint log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-lint - path: build/nox-lint-output.log - retention-days: 30 - typecheck: runs-on: docker container: @@ -81,19 +71,10 @@ jobs: - name: Run typecheck via nox run: | - mkdir -p build - nox -s typecheck 2>&1 | tee build/nox-typecheck-output.log + nox -s typecheck env: NOX_DEFAULT_VENV_BACKEND: uv - - name: Upload typecheck log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-typecheck - path: build/nox-typecheck-output.log - retention-days: 30 - security: runs-on: docker container: @@ -119,25 +100,16 @@ jobs: - name: Run security scan via nox run: | - mkdir -p build - nox -s security_scan 2>&1 | tee build/nox-security-output.log + nox -s security_scan env: NOX_DEFAULT_VENV_BACKEND: uv - name: Run dead code detection via nox run: | - nox -s dead_code 2>&1 | tee -a build/nox-security-output.log + nox -s dead_code env: NOX_DEFAULT_VENV_BACKEND: uv - - name: Upload security log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-security - path: build/nox-security-output.log - retention-days: 30 - quality: runs-on: docker container: @@ -163,23 +135,18 @@ jobs: - name: Run complexity check via nox run: | - mkdir -p build - nox -s complexity 2>&1 | tee build/nox-quality-output.log + nox -s complexity env: NOX_DEFAULT_VENV_BACKEND: uv - - name: Upload quality log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-quality - path: build/nox-quality-output.log - retention-days: 30 - unit_tests: runs-on: docker + strategy: + matrix: + python-version: ["3.13"] + fail-fast: false container: - image: python:3.13-slim + image: python:${{ matrix.python-version }}-slim steps: - name: Install system dependencies (nodejs for checkout, git for merge tests, curl/tar for Helm) run: | @@ -208,29 +175,24 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/uv - key: uv-${{ hashFiles('pyproject.toml') }} + key: uv-tests-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} restore-keys: | - uv- + uv-tests-${{ matrix.python-version }}- - name: Run unit tests via nox run: | - mkdir -p build - nox -s unit_tests 2>&1 | tee build/nox-unit-tests-output.log + nox -s "unit_tests-${{ matrix.python-version }}" env: NOX_DEFAULT_VENV_BACKEND: uv - - name: Upload unit tests log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-unit-tests - path: build/nox-unit-tests-output.log - retention-days: 30 - integration_tests: runs-on: docker + strategy: + matrix: + python-version: ["3.13"] + fail-fast: false container: - image: python:3.13-slim + image: python:${{ matrix.python-version }}-slim steps: - name: Install system dependencies (nodejs for checkout, git for integration tests, curl/tar for Helm) run: | @@ -259,14 +221,13 @@ jobs: uses: actions/cache@v3 with: path: ~/.cache/uv - key: uv-${{ hashFiles('pyproject.toml') }} + key: uv-tests-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} restore-keys: | - uv- + uv-tests-${{ matrix.python-version }}- - name: Run integration tests via nox run: | - mkdir -p build - nox -s integration_tests 2>&1 | tee build/nox-integration-tests-output.log + nox -s "integration_tests-${{ matrix.python-version }}" env: NOX_DEFAULT_VENV_BACKEND: uv CLEVERAGENTS_REQUIRE_HELM_RENDER_ASSERTIONS: "true" @@ -277,14 +238,6 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - - name: Upload integration tests log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-integration-tests - path: build/nox-integration-tests-output.log - retention-days: 30 - e2e_tests: runs-on: docker timeout-minutes: 45 @@ -311,31 +264,18 @@ jobs: - name: Run E2E tests via nox run: | - mkdir -p build - nox -s e2e_tests 2>&1 | tee build/nox-e2e-tests-output.log + nox -s e2e_tests env: NOX_DEFAULT_VENV_BACKEND: uv - # Run E2E suites in parallel via pabot. 4 workers keeps - # wall-clock time well under the 45-minute timeout while - # staying within the memory budget of the docker runner. - TEST_PROCESSES: "4" ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} - - name: Upload E2E tests log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-e2e-tests - path: build/nox-e2e-tests-output.log - retention-days: 30 - coverage: runs-on: docker container: image: python:3.13-slim - needs: [lint, typecheck, security, quality] + needs: [lint, typecheck] steps: - name: Install system dependencies (nodejs for checkout, git for merge tests) run: | @@ -359,9 +299,9 @@ jobs: id: coverage run: | mkdir -p build - nox -s coverage_report 2>&1 | tee build/nox-coverage-output.log + nox -s coverage_report 2>&1 | tee build/coverage-output.txt # Extract the single-line CI summary from nox output - grep -E '^(nox > )?COVERAGE (OK|FAILED):' build/nox-coverage-output.log || true + grep -E '^(nox > )?COVERAGE (OK|FAILED):' build/coverage-output.txt || true env: NOX_DEFAULT_VENV_BACKEND: uv @@ -375,7 +315,7 @@ jobs: data = json.load(f) summary = data.get('summary') or data.get('totals') or {} pct = round(summary.get('percent_covered', 0), 1) - threshold = 50 # Temporarily lowered; see issues #4183 and #4184 + threshold = 97 if pct >= threshold: print(f'COVERAGE OK: {pct}% (threshold: {threshold}%)') else: @@ -387,14 +327,6 @@ jobs: exit 1 fi - - name: Upload coverage log artifact - if: always() - uses: actions/upload-artifact@v3 - with: - name: ci-logs-coverage - path: build/nox-coverage-output.log - retention-days: 30 - - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v3 @@ -406,6 +338,120 @@ jobs: build/htmlcov/ retention-days: 30 + benchmark-regression: + if: forgejo.event_name == 'pull_request' + runs-on: docker-benchmark + container: + image: python:3.13-slim + needs: [lint, typecheck] + steps: + - name: Install system dependencies (nodejs for checkout, git for merge tests) + run: | + apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/* + + - name: Checkout full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Compute base commit + id: hash + run: | + git fetch origin "${{ forgejo.base_ref }}" --depth=200 + BASE_SHA=$(git merge-base HEAD "origin/${{ forgejo.base_ref }}") + echo "ASV_BASE_SHA=${BASE_SHA}" >> $FORGEJO_OUTPUT + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install asv virtualenv uv==${{ env.UV_VERSION }} nox + + - name: Restore prior ASV benchmarks + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + ASV_S3_BUCKET: ${{ secrets.ASV_S3_BUCKET }} + run: | + python -m pip install awscli + mkdir -p build/asv/results + aws s3 sync "s3://${ASV_S3_BUCKET}/asv/results" build/asv/results --delete || true + + - name: Run asv continuous via nox + env: + ASV_BASE_SHA: ${{ steps.hash.outputs.ASV_BASE_SHA }} + run: | + nox -s benchmark_regression + + - name: Archive the results + run: | + tar cf /tmp/asv-results.tar build/asv/results build/asv/html + + - name: Upload benchmark artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: asv-results-pr + path: /tmp/asv-results.tar + rentention-days: 30 + + benchmark-publish: + if: forgejo.event_name == 'push' && ( forgejo.ref == 'refs/heads/master' || forgejo.ref == 'refs/heads/develop' ) + runs-on: docker-benchmark + container: python:3.13-slim + steps: + - name: Install system dependencies (nodejs for checkout, git for merge tests) + run: | + apt-get update && apt-get install -y -qq nodejs git && rm -rf /var/lib/apt/lists/* + + - name: Checkout full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install asv virtualenv uv==${{ env.UV_VERSION }} nox + + - name: Restore prior ASV benchmarks + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + ASV_S3_BUCKET: ${{ secrets.ASV_S3_BUCKET }} + run: | + python -m pip install awscli + mkdir -p build/asv/results + aws s3 sync "s3://${ASV_S3_BUCKET}/asv/results" build/asv/results --delete || true + + - name: Run asv via nox + run: | + nox -s benchmark + + - name: Upload updated benchmarks and website to S3 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + ASV_S3_BUCKET: ${{ secrets.ASV_S3_BUCKET }} + run: | + python -m pip install awscli + aws s3 sync build/asv/results "s3://${ASV_S3_BUCKET}/asv/results" --delete + aws s3 sync build/asv/html "s3://${ASV_S3_BUCKET}/asv/html" --delete + + - name: Archive the results + run: | + tar cf /tmp/asv-results.tar build/asv/results build/asv/html + + - name: Upload benchmark artifacts + if: always() + uses: actions/upload-artifact@v3 + with: + name: asv-results-pr + path: /tmp/asv-results.tar + rentention-days: 30 + build: runs-on: docker container: @@ -436,7 +482,7 @@ jobs: NOX_DEFAULT_VENV_BACKEND: uv docker: - needs: [lint, typecheck, security, quality, unit_tests] + needs: [lint, typecheck, unit_tests, security] runs-on: docker container: image: docker:dind @@ -522,88 +568,9 @@ jobs: -summary \ /tmp/rendered.yaml - - push-validation: - # Validates that the CI runner can authenticate and push to the repository. - # Root cause of the push failure: actions/checkout@v4 was not configured with - # explicit push credentials (token + persist-credentials), and no git user - # config (name/email) was set — both are required for any push operation. - runs-on: docker - container: - image: python:3.13-slim - steps: - - name: Install system dependencies (nodejs for checkout, git for push validation) - run: | - apt-get update && apt-get install -y -qq nodejs git curl && rm -rf /var/lib/apt/lists/* - - - name: Checkout with explicit write credentials - uses: actions/checkout@v4 - with: - # Pass the Forgejo token explicitly so the credential helper is - # configured for HTTPS push operations. Without this, the default - # checkout may only have read access and push will fail with a - # 403 or authentication error. - token: ${{ secrets.FORGEJO_TOKEN }} - persist-credentials: true - - - name: Configure git user for CI operations - run: | - # Required for any git commit or push operation in CI. - # Uses a bot identity to distinguish CI-generated commits from - # human commits. Without this, git push fails with: - # "Author identity unknown — please tell me who you are." - git config user.name "CleverAgents CI" - git config user.email "ci-bot@cleverthis.com" - - - name: Verify HTTPS credential helper is configured - run: | - # Confirm that the credential helper set up by actions/checkout - # is active. This ensures HTTPS push operations will authenticate - # correctly without prompting for a password. - echo "=== Git credential configuration ===" - git config --list | grep -E "credential|url" || echo "WARNING: No credential helper found" - echo "=== Remote URL ===" - git remote get-url origin - echo "=== Credential helper check ===" - if git config credential.helper > /dev/null 2>&1; then - echo "OK: Credential helper is configured: $(git config credential.helper)" - else - echo "WARNING: No credential helper configured — push may fail" - fi - - - name: Smoke-test push access via API - # Validates write permission using the Forgejo API before attempting - # any real push. This catches credential issues early with a clear - # error message rather than a cryptic git error. - env: - FORGEJO_URL: ${{ secrets.FORGEJO_URL }} - FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} - run: | - REPO="${{ forgejo.repository }}" - API_URL="${FORGEJO_URL}/api/v1/repos/${REPO}" - echo "=== Testing repository API access ===" - HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: token ${FORGEJO_TOKEN}" \ - "${API_URL}") - if [ "${HTTP_STATUS}" != "200" ]; then - echo "ERROR: FORGEJO_TOKEN cannot access repository API (HTTP ${HTTP_STATUS})." - echo "Ensure FORGEJO_TOKEN is set in Repository Settings > Actions > Secrets" - echo "and that the token has repository (write) scope." - exit 1 - fi - PUSH_ALLOWED=$(curl -s \ - -H "Authorization: token ${FORGEJO_TOKEN}" \ - "${API_URL}" | python3 -c "import sys,json; d=json.load(sys.stdin); print(str(d.get('permissions',{}).get('push',False)).lower())") - if [ "${PUSH_ALLOWED}" != "true" ]; then - echo "ERROR: FORGEJO_TOKEN does not have push (write) permission." - echo "Grant the token Contents: Write permission or use a token with full repository scope." - exit 1 - fi - echo "OK: Push access verified -- FORGEJO_TOKEN has write permission on ${REPO}" - echo "=== Push access smoke-test passed ===" status-check: if: always() - needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm, push-validation] + needs: [lint, typecheck, security, quality, unit_tests, integration_tests, e2e_tests, coverage, build, docker, helm] runs-on: docker container: image: python:3.13-slim @@ -621,7 +588,6 @@ jobs: echo "build: ${{ needs.build.result }}" echo "docker: ${{ needs.docker.result }}" echo "helm: ${{ needs.helm.result }}" - echo "push-validation: ${{ needs.push-validation.result }}" if [ "${{ needs.lint.result }}" != "success" ] || \ [ "${{ needs.typecheck.result }}" != "success" ] || \ @@ -633,8 +599,7 @@ jobs: [ "${{ needs.coverage.result }}" != "success" ] || \ [ "${{ needs.build.result }}" != "success" ] || \ [ "${{ needs.docker.result }}" != "success" ] || \ - [ "${{ needs.helm.result }}" != "success" ] || \ - [ "${{ needs.push-validation.result }}" != "success" ]; then + [ "${{ needs.helm.result }}" != "success" ]; then echo "FAILED: One or more required jobs did not succeed" exit 1 fi diff --git a/features/steps/tui_first_run_steps.py b/features/steps/tui_first_run_steps.py index 590b4dead..e2e210f91 100644 --- a/features/steps/tui_first_run_steps.py +++ b/features/steps/tui_first_run_steps.py @@ -233,7 +233,7 @@ def step_render_with_query(context: object, query: str) -> None: context._rendered = render_actor_selection(_DEFAULT_ACTORS, 0, query) -@then('the rendered text should contain "{text}"') +@then('the actor selection rendered text should contain "{text}"') def step_rendered_contains(context: object, text: str) -> None: assert text in context._rendered, ( f"Expected '{text}' in rendered text:\n{context._rendered}" diff --git a/features/tui_first_run.feature b/features/tui_first_run.feature index 20e18d55d..b314420e0 100644 --- a/features/tui_first_run.feature +++ b/features/tui_first_run.feature @@ -36,11 +36,11 @@ Feature: TUI first-run experience with actor selection overlay Scenario: render_actor_selection includes welcome header When I render the actor selection with default actors - Then the rendered text should contain "Welcome to CleverAgents" + Then the actor selection rendered text should contain "Welcome to CleverAgents" Scenario: render_actor_selection marks the first actor as recommended When I render the actor selection with default actors - Then the rendered text should contain "(recommended)" + Then the actor selection rendered text should contain "(recommended)" Scenario: render_actor_selection highlights the selected actor with cursor When I render the actor selection with selected index 2 @@ -48,17 +48,17 @@ Feature: TUI first-run experience with actor selection overlay Scenario: render_actor_selection shows search prompt when query is empty When I render the actor selection with default actors - Then the rendered text should contain "to search..." + Then the actor selection rendered text should contain "to search..." Scenario: render_actor_selection shows active search query When I render the actor selection with search query "claude" - Then the rendered text should contain "claude" + Then the actor selection rendered text should contain "claude" Scenario: render_actor_selection includes footer keybindings When I render the actor selection with default actors - Then the rendered text should contain "enter Select" - And the rendered text should contain "j/k Navigate" - And the rendered text should contain "/ Search" + Then the actor selection rendered text should contain "enter Select" + And the actor selection rendered text should contain "j/k Navigate" + And the actor selection rendered text should contain "/ Search" # ----------------------------------------------------------------------- # ActorSelectionOverlay widget diff --git a/src/cleveragents/cli/commands/session.py b/src/cleveragents/cli/commands/session.py index 25229f840..e155d4f76 100644 --- a/src/cleveragents/cli/commands/session.py +++ b/src/cleveragents/cli/commands/session.py @@ -98,11 +98,11 @@ def _facade_dispatch(operation: str, params: dict[str, Any]) -> dict[str, Any]: from cleveragents.a2a.cli_bootstrap import get_facade facade = get_facade() - request = A2aRequest(method=operation, params=params) + request = A2aRequest(operation=operation, params=params) response = facade.dispatch(request) - if response.error is not None: + if response.status == "error" and response.error is not None: raise RuntimeError(response.error.message) - return dict(response.result or {}) + return dict(response.data) # --------------------------------------------------------------------------- @@ -219,8 +219,9 @@ def create( console.print(Panel(details, title="Session", expand=False)) # Settings panel + automation = session.metadata.get("automation_profile") or "default" settings_text = ( - "[yellow]Automation:[/yellow] default\n" + f"[yellow]Automation:[/yellow] {automation}\n" "[yellow]Streaming:[/yellow] off\n" "[yellow]Context:[/yellow] default\n" "[yellow]Memory:[/yellow] enabled\n" @@ -324,18 +325,21 @@ def list_sessions( console.print(table) console.print() - # Reuse the summary dict already computed by _session_list_dict to avoid - # duplicating the total_msgs / sorted_sessions / most_recent / oldest logic. - summary = data["summary"] + # Summary panel with all required fields + total_msgs = sum(s.message_count for s in sessions) + # Find most recent and oldest sessions + sorted_sessions = sorted(sessions, key=lambda x: x.updated_at, reverse=True) + most_recent = sorted_sessions[0].name or sorted_sessions[0].session_id[:8] + oldest = sorted_sessions[-1].name or sorted_sessions[-1].session_id[:8] summary_table = Table.grid(padding=(0, 1)) summary_table.add_column(style="cyan bold", justify="left") summary_table.add_column(style="white", justify="left") - summary_table.add_row("Total:", str(summary["total"])) - summary_table.add_row("Most Recent:", summary["most_recent"] or "") - summary_table.add_row("Oldest:", summary["oldest"] or "") - summary_table.add_row("Total Messages:", str(summary["total_messages"])) - summary_table.add_row("Storage:", summary["storage"]) + summary_table.add_row("Total:", str(len(sessions))) + summary_table.add_row("Most Recent:", most_recent) + summary_table.add_row("Oldest:", oldest) + summary_table.add_row("Total Messages:", str(total_msgs)) + summary_table.add_row("Storage:", "0 KB") # Placeholder console.print(Panel(summary_table, title="Summary", border_style="blue")) console.print() @@ -370,15 +374,16 @@ def show( typer.echo(format_output(dict(data), fmt)) return - # Session summary panel — field order per spec: ID, Actor, Messages, - # Created, Updated, Automation (docs/specification.md §agents session show) + # Session summary panel + automation = session.metadata.get("automation_profile") or "(none)" details = ( - f"[bold]ID:[/bold] {session.session_id}\n" + f"[bold]Session ID:[/bold] {session.session_id}\n" f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n" + f"[bold]Namespace:[/bold] {session.namespace}\n" f"[bold]Messages:[/bold] {session.message_count}\n" f"[bold]Created:[/bold] {session.created_at.strftime('%Y-%m-%d %H:%M')}\n" f"[bold]Updated:[/bold] {session.updated_at.strftime('%Y-%m-%d %H:%M')}\n" - f"[bold]Automation:[/bold] {session.automation or '(none)'}" + f"[bold]Automation:[/bold] {automation}" ) console.print(Panel(details, title="Session Summary", expand=False)) @@ -387,33 +392,29 @@ def show( recent = session.messages[-5:] msg_table = Table(title="Recent Messages", show_header=True) msg_table.add_column("Role", style="cyan") - msg_table.add_column("Text") + msg_table.add_column("Content") + msg_table.add_column("Timestamp", style="dim") for msg in recent: - text = msg.content - if len(text) > 80: - text = text[:77] + "..." - msg_table.add_row(msg.role.value, text) + content = msg.content + if len(content) > 80: + content = content[:77] + "..." + msg_table.add_row( + msg.role.value, + content, + msg.timestamp.strftime("%H:%M:%S"), + ) console.print(msg_table) - # Linked plans — spec requires Plan ID / Phase / State columns - if session.linked_plans: - plan_table = Table(title="Linked Plans", show_header=True) - plan_table.add_column("Plan ID", style="cyan") - plan_table.add_column("Phase") - plan_table.add_column("State") - for lp in session.linked_plans: - plan_table.add_row(lp.plan_id, lp.phase, lp.state) - console.print(Panel(plan_table, title="Linked Plans", expand=False)) - elif session.linked_plan_ids: - # Fallback: only flat IDs available + # Linked plans + if session.linked_plan_ids: plan_text = "\n".join(f" • {pid}" for pid in session.linked_plan_ids) console.print(Panel(plan_text, title="Linked Plans", expand=False)) # Token usage tu = session.token_usage usage_text = ( - f"[blue]Input Tokens:[/blue] {tu.input_tokens:,}\n" - f"[blue]Output Tokens:[/blue] {tu.output_tokens:,}\n" + f"[blue]Input Tokens:[/blue] {tu.input_tokens}\n" + f"[blue]Output Tokens:[/blue] {tu.output_tokens}\n" f"[yellow]Estimated Cost:[/yellow] ${tu.estimated_cost:.4f}" ) console.print(Panel(usage_text, title="Token Usage", expand=False)) @@ -442,6 +443,8 @@ def show( console.print("[green bold]✓ OK[/green bold] Session details loaded") + console.print("[green bold]✓ OK[/green bold] Session details loaded") + except SessionNotFoundError as exc: console.print(f"[red]Session not found:[/red] {session_id}") raise typer.Exit(1) from exc @@ -480,9 +483,8 @@ def delete( try: service = _get_session_service() - # Verify session exists before prompting and capture message count - session_to_delete = service.get(session_id) - message_count = session_to_delete.message_count + # Verify session exists before prompting + session = service.get(session_id) if not yes: confirm = typer.confirm(f"Delete session {session_id}?", default=False) @@ -490,6 +492,9 @@ def delete( console.print("[yellow]Aborted.[/yellow]") raise typer.Abort() + # Get message count before deletion + message_count = session.message_count + service.delete(session_id) # Rich output: render Deletion Summary and Cleanup panels @@ -565,11 +570,6 @@ def export_session( JSON (canonical, importable). Use ``--format md`` for a human-readable Markdown transcript (lossy — cannot be re-imported). - On success, three Rich panels are displayed: "Session Export" (session ID, - output path, message count, file size, format), "Contents" (messages, plan - references, metadata keys, actor config, schema version), and "Integrity" - (checksum, encrypted flag). - Examples: agents session export 01HXYZ... agents session export 01HXYZ... -o session.json @@ -582,13 +582,12 @@ def export_session( try: service = _get_session_service() - json_data: dict[str, Any] = {} if fmt == "md": # Markdown export: load full session with messages session = service.get(session_id) # Load messages via export_session to populate session.messages - json_data = service.export_session(session_id) + export_data = service.export_session(session_id) messages = [ SessionMessage( message_id=m["message_id"], @@ -599,13 +598,13 @@ def export_session( metadata=m.get("metadata", {}), tool_call_id=m.get("tool_call_id"), ) - for m in json_data.get("messages", []) + for m in export_data.get("messages", []) ] session.messages = messages content = session.as_export_markdown() else: - json_data = service.export_session(session_id) - content = json.dumps(json_data, indent=2, default=str) + data = service.export_session(session_id) + content = json.dumps(data, indent=2, default=str) if output is not None: if output.exists() and not force: @@ -617,108 +616,16 @@ def export_session( # Create parent directories if needed output.parent.mkdir(parents=True, exist_ok=True) output.write_text(content, encoding="utf-8") + console.print(f"[green]✓ OK[/green] Session exported to {output}") else: typer.echo(content) - # Render Rich panels for both file and stdout export paths - _render_export_panels( - session_id=session_id, - output=output, - content=content, - export_data=json_data, - fmt=fmt, - ) - except SessionNotFoundError as exc: console.print(f"[red]Session not found:[/red] {session_id}") raise typer.Exit(1) from exc except SessionExportError as exc: console.print(f"[red]Export error:[/red] {exc}") raise typer.Exit(1) from exc - except DatabaseError as exc: - _log.debug("session export failed", exc_info=True) - console.print( - f"[red]Error:[/red] Database unavailable: {exc}\n" - "Hint: run 'agents init' to initialise the database." - ) - raise typer.Exit(1) from exc - - -def _render_export_panels( - *, - session_id: str, - output: Path | None, - content: str, - export_data: dict[str, Any], - fmt: str, -) -> None: - """Render the three spec-required Rich panels for ``agents session export``. - - Displays: - - "Session Export" panel: session ID, output path, message count, size, format - - "Contents" panel: messages, plan references, metadata keys, actor config, - schema version - - "Integrity" panel: checksum, encrypted flag - - ``✓ OK Export completed`` success line - """ - # Compute file size from content - size_bytes = len(content.encode("utf-8")) - if size_bytes < 1024: - size_str = f"{size_bytes} B" - elif size_bytes < 1024 * 1024: - size_str = f"{size_bytes // 1024} KB" - else: - size_str = f"{size_bytes // (1024 * 1024)} MB" - - # Derive display values from export data (JSON format) or defaults (md) - message_count = len(export_data.get("messages", [])) - plan_refs = len(export_data.get("linked_plan_ids", [])) - metadata_keys = len(export_data.get("metadata", {})) - actor_config = "included" if export_data.get("actor_name") else "none" - schema_version = export_data.get("schema_version", "v1") - checksum_raw = export_data.get("checksum", "") - checksum_display = ( - f"sha256:{checksum_raw[:4]}...{checksum_raw[-4:]}" - if len(checksum_raw) >= 8 - else checksum_raw or "n/a" - ) - output_display = str(output) if output is not None else "(stdout)" - format_display = "JSON" if fmt == "json" else "Markdown" - - # Session Export panel - export_table = Table.grid(padding=(0, 1)) - export_table.add_column(style="cyan bold", justify="left") - export_table.add_column(style="white", justify="left") - export_table.add_row("Session:", session_id) - export_table.add_row("Output:", output_display) - export_table.add_row("Messages:", str(message_count)) - export_table.add_row("Size:", size_str) - export_table.add_row("Format:", format_display) - console.print(Panel(export_table, title="Session Export", border_style="blue")) - console.print() - - # Contents panel - contents_table = Table.grid(padding=(0, 1)) - contents_table.add_column(style="blue bold", justify="left") - contents_table.add_column(style="white", justify="left") - contents_table.add_row("Messages:", str(message_count)) - contents_table.add_row("Plan References:", str(plan_refs)) - contents_table.add_row("Metadata Keys:", str(metadata_keys)) - contents_table.add_row("Actor Config:", actor_config) - contents_table.add_row("Schema Version:", str(schema_version)) - console.print(Panel(contents_table, title="Contents", border_style="blue")) - console.print() - - # Integrity panel - integrity_table = Table.grid(padding=(0, 1)) - integrity_table.add_column(style="magenta bold", justify="left") - integrity_table.add_column(style="white", justify="left") - integrity_table.add_row("Checksum:", checksum_display) - integrity_table.add_row("Encrypted:", "no") - console.print(Panel(integrity_table, title="Integrity", border_style="blue")) - console.print() - - console.print("[green]✓ OK[/green] Export completed") @app.command("import") @@ -749,44 +656,20 @@ def import_session( try: service = _get_session_service() - schema_version = data.get("schema_version", "unknown") - actor_name = data.get("actor_name") session = service.import_session(data) - # Session Import panel - session_details = ( - f"[bold]Input:[/bold] {input_file}\n" + details = ( f"[bold]Session ID:[/bold] {session.session_id}\n" + f"[bold]Actor:[/bold] {session.actor_name or '(none)'}\n" f"[bold]Messages:[/bold] {session.message_count}\n" - f"[bold]Schema:[/bold] {schema_version}" + f"[bold]Namespace:[/bold] {session.namespace}" ) - console.print(Panel(session_details, title="Session Import", expand=False)) - - # Validation panel - actor_ref_status = "resolved" if actor_name else "none" - validation_details = ( - f"[bold]Checksum:[/bold] verified\n" - f"[bold]Schema:[/bold] compatible\n" - f"[bold]Actor Ref:[/bold] {actor_ref_status}" - ) - console.print(Panel(validation_details, title="Validation", expand=False)) - - # Merge panel - merge_details = "[bold]Existing:[/bold] none\n[bold]Strategy:[/bold] create new" - console.print(Panel(merge_details, title="Merge", expand=False)) - - console.print("[green]✓ OK[/green] Import completed") + console.print(Panel(details, title="Session Imported", expand=False)) + console.print("[green]✓ OK[/green] Session imported") except SessionImportError as exc: console.print(f"[red]Import error:[/red] {exc}") raise typer.Exit(1) from exc - except DatabaseError as exc: - _log.debug("session import failed", exc_info=True) - console.print( - f"[red]Error:[/red] Database unavailable: {exc}\n" - "Hint: run 'agents init' to initialise the database." - ) - raise typer.Exit(1) from exc @app.command() @@ -855,10 +738,3 @@ def tell( except SessionNotFoundError as exc: console.print(f"[red]Session not found:[/red] {session_id}") raise typer.Exit(1) from exc - except DatabaseError as exc: - _log.debug("session tell failed", exc_info=True) - console.print( - f"[red]Error:[/red] Database unavailable: {exc}\n" - "Hint: run 'agents init' to initialise the database." - ) - raise typer.Exit(1) from exc diff --git a/vulture_whitelist.py b/vulture_whitelist.py index 991afa4b2..3e194e557 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -7,10 +7,6 @@ # Context manager __exit__ parameters required by protocol exc_tb # noqa: B018, F821 -# OutputMaterializerExtension.materialize and A2ATransportExtension.send protocol -# parameters — required by interface definition but not used in abstract body -destination # noqa: B018, F821 - # Legacy migrator method parameter needed for mapping interface consistency build_data # noqa: B018, F821 @@ -1214,5 +1210,6 @@ selective_rollback # noqa: B018, F821 archive_artifacts # noqa: B018, F821 revert_decisions # noqa: B018, F821 -# Extension protocol parameters — required by Protocol interface definitions +# Extension protocol method parameters — required by Protocol interface definitions +# OutputMaterializerExtension.materialize() and A2ATransportExtension.send() destination # noqa: B018, F821