devops-bot-sdk 1.6.68__tar.gz → 1.6.70__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/SOURCES.txt +5 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/os_setup.py +5 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/shipper.py +46 -1
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/activity.py +1 -1
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/chat.py +51 -1
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/client.py +39 -8
- devops_bot_sdk-1.6.70/sdk/schedule_tickets.py +201 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/scheduler.py +119 -15
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/vps_health.py +9 -1
- devops_bot_sdk-1.6.70/sdk/vps_identity.py +174 -0
- devops_bot_sdk-1.6.70/tests/test_schedule_tickets.py +218 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_scheduler.py +36 -0
- devops_bot_sdk-1.6.70/tests/test_scheduler_transient_retry.py +230 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_vps_health.py +9 -0
- devops_bot_sdk-1.6.70/tests/test_vps_only_reporting.py +234 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/README.md +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.6.68 → devops_bot_sdk-1.6.70}/tests/test_write_hook.py +0 -0
|
@@ -38,6 +38,7 @@ sdk/py.typed
|
|
|
38
38
|
sdk/resume.py
|
|
39
39
|
sdk/router.py
|
|
40
40
|
sdk/run_auto.py
|
|
41
|
+
sdk/schedule_tickets.py
|
|
41
42
|
sdk/scheduler.py
|
|
42
43
|
sdk/self_report.py
|
|
43
44
|
sdk/self_report_local.py
|
|
@@ -52,6 +53,7 @@ sdk/tickets.py
|
|
|
52
53
|
sdk/updater.py
|
|
53
54
|
sdk/url_access.py
|
|
54
55
|
sdk/vps_health.py
|
|
56
|
+
sdk/vps_identity.py
|
|
55
57
|
sdk/write_hook.py
|
|
56
58
|
sdk/activity/__init__.py
|
|
57
59
|
sdk/activity/classifier.py
|
|
@@ -203,7 +205,9 @@ tests/test_run_outputs.py
|
|
|
203
205
|
tests/test_run_verdict_is_honest.py
|
|
204
206
|
tests/test_schedule_api.py
|
|
205
207
|
tests/test_schedule_outputs_and_plans.py
|
|
208
|
+
tests/test_schedule_tickets.py
|
|
206
209
|
tests/test_scheduler.py
|
|
210
|
+
tests/test_scheduler_transient_retry.py
|
|
207
211
|
tests/test_self_report.py
|
|
208
212
|
tests/test_service_token_warning.py
|
|
209
213
|
tests/test_stale_panel_restart.py
|
|
@@ -223,6 +227,7 @@ tests/test_updater_pep668.py
|
|
|
223
227
|
tests/test_updater_reports_the_truth.py
|
|
224
228
|
tests/test_url_access.py
|
|
225
229
|
tests/test_vps_health.py
|
|
230
|
+
tests/test_vps_only_reporting.py
|
|
226
231
|
tests/test_windows_loop_identity.py
|
|
227
232
|
tests/test_windows_task_paths.py
|
|
228
233
|
tests/test_write_hook.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.70
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.70" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
38
38
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
39
39
|
|
|
40
40
|
from sdk.client import BackendClient
|
|
@@ -188,6 +188,11 @@ def _flag_value() -> str:
|
|
|
188
188
|
return value.strip().strip('"').strip("'")
|
|
189
189
|
except OSError:
|
|
190
190
|
pass
|
|
191
|
+
# Under sudo, settings.setting reads ROOT's .env. Falling back to it made a
|
|
192
|
+
# flag left in /root/.agentos/.env answer for the panel user, so _enable_flag
|
|
193
|
+
# never wrote theirs and the collector never started — on every restart.
|
|
194
|
+
if _is_privileged() and _panel_user() is not None:
|
|
195
|
+
return ""
|
|
191
196
|
return settings.setting(AUDIT_FLAG)
|
|
192
197
|
|
|
193
198
|
|
|
@@ -176,12 +176,57 @@ async def flush_once(
|
|
|
176
176
|
return result
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
def discard_spool(spool: Spool | None = None) -> int:
|
|
180
|
+
"""Delete everything queued, without sending it. Returns the rows dropped."""
|
|
181
|
+
spool = spool or settings.get_spool()
|
|
182
|
+
dropped = 0
|
|
183
|
+
while True:
|
|
184
|
+
chunk = spool.next_chunk()
|
|
185
|
+
if not chunk:
|
|
186
|
+
return dropped
|
|
187
|
+
dropped += len(chunk)
|
|
188
|
+
spool.ack(chunk)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def ship_tick(
|
|
192
|
+
is_vps: Callable[[], Awaitable[bool | None]] | None = None,
|
|
193
|
+
) -> FlushResult | None:
|
|
194
|
+
"""One cron tick: ship only from the caller's assigned VPS.
|
|
195
|
+
|
|
196
|
+
The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
|
|
197
|
+
with the same token put its own events in the VPS's audit trail. On a machine
|
|
198
|
+
that is definitely not the VPS the queue is discarded — kept, it would only grow
|
|
199
|
+
forever, since that machine will never be allowed to send it. When the answer
|
|
200
|
+
is unknown (profile unreachable) everything is kept for the next tick.
|
|
201
|
+
"""
|
|
202
|
+
if is_vps is None:
|
|
203
|
+
from sdk import vps_identity
|
|
204
|
+
is_vps = vps_identity.is_assigned_vps
|
|
205
|
+
try:
|
|
206
|
+
verdict = await is_vps()
|
|
207
|
+
except Exception: # noqa: BLE001 — is_assigned_vps never raises, but be certain
|
|
208
|
+
verdict = None
|
|
209
|
+
if verdict is None:
|
|
210
|
+
return None
|
|
211
|
+
if verdict is False:
|
|
212
|
+
try:
|
|
213
|
+
dropped = discard_spool()
|
|
214
|
+
except Exception as exc: # noqa: BLE001
|
|
215
|
+
logger.warning("activity: could not discard the spool: %s", exc)
|
|
216
|
+
return None
|
|
217
|
+
if dropped:
|
|
218
|
+
logger.info("activity.not_assigned_vps discarded=%d (this machine is not the "
|
|
219
|
+
"user's VPS; nothing is shipped from it)", dropped)
|
|
220
|
+
return None
|
|
221
|
+
return await flush_once()
|
|
222
|
+
|
|
223
|
+
|
|
179
224
|
async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
|
|
180
225
|
"""Flush every ``interval_s`` (default 5 minutes) until cancelled."""
|
|
181
226
|
await asyncio.sleep(initial_delay_s)
|
|
182
227
|
while True:
|
|
183
228
|
try:
|
|
184
|
-
await
|
|
229
|
+
await ship_tick()
|
|
185
230
|
every = interval_s if interval_s is not None else settings.ship_interval_seconds()
|
|
186
231
|
await asyncio.sleep(every)
|
|
187
232
|
except asyncio.CancelledError:
|
|
@@ -79,7 +79,7 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
|
79
79
|
# per-run event. So an agent run costs one POST no matter how chatty it is.
|
|
80
80
|
_LINE_BUF: dict[str, list[str]] = {}
|
|
81
81
|
_LINE_BUF_MAX_LINES = 40 # keep the tail — recent lines are the useful ones
|
|
82
|
-
_LINE_BUF_MAX_CHARS = 4000 #
|
|
82
|
+
_LINE_BUF_MAX_CHARS = 4000 # trimmed to the gateway limit by client._fit_activity
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def note(bucket: str, text: str) -> None:
|
|
@@ -41,6 +41,52 @@ def _tee_events(log_path, on_event: "EventHandler | None") -> "EventHandler":
|
|
|
41
41
|
EventHandler = Callable[[dict], Awaitable[None]]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
#: Pauses between attempts when an agent list cannot be read. The AI backend
|
|
45
|
+
#: verifies the co_ token against the ML gateway on every call and that check
|
|
46
|
+
#: flakes (a spurious 401 or a dropped connection for a few seconds), which made a
|
|
47
|
+
#: scheduled run give up on the first miss. Short, because a human may be waiting.
|
|
48
|
+
_LOOKUP_RETRY_DELAYS_S: tuple[float, ...] = (2.0, 5.0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def _fetch_rows(fetch) -> list[dict]:
|
|
52
|
+
"""``fetch()`` with brief retries; raises the last error when every attempt fails."""
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
55
|
+
last: Exception | None = None
|
|
56
|
+
for delay in (0.0, *_LOOKUP_RETRY_DELAYS_S):
|
|
57
|
+
if delay:
|
|
58
|
+
await asyncio.sleep(delay)
|
|
59
|
+
try:
|
|
60
|
+
return await fetch() or []
|
|
61
|
+
except Exception as exc: # noqa: BLE001 — classified by the caller
|
|
62
|
+
last = exc
|
|
63
|
+
assert last is not None
|
|
64
|
+
raise last
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async def _gateway_manifest(client, name: str):
|
|
68
|
+
"""The agent from the ML gateway's listing, or ``None``.
|
|
69
|
+
|
|
70
|
+
The gateway reads the same ``agents`` table the AI backend re-projects, with no
|
|
71
|
+
second token-verification hop, so it stays readable when the backend flakes.
|
|
72
|
+
Consulted only after a backend source could not be read.
|
|
73
|
+
"""
|
|
74
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
75
|
+
|
|
76
|
+
fetch = getattr(client, "list_gateway_agents_by_scope", None)
|
|
77
|
+
if fetch is None:
|
|
78
|
+
return None
|
|
79
|
+
try:
|
|
80
|
+
scopes = await fetch() or {}
|
|
81
|
+
except Exception: # noqa: BLE001 — the fallback failing changes nothing
|
|
82
|
+
return None
|
|
83
|
+
for scope in ("personal", "department"):
|
|
84
|
+
for row in scopes.get(scope) or []:
|
|
85
|
+
if isinstance(row, dict) and row.get("name") == name:
|
|
86
|
+
return manifest_from_backend_row(row)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
44
90
|
class AgentLookupUnavailable(LookupError):
|
|
45
91
|
"""The agent could not be found, but at least one source could not be READ.
|
|
46
92
|
|
|
@@ -88,7 +134,7 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
|
|
|
88
134
|
if fetch is None:
|
|
89
135
|
continue
|
|
90
136
|
try:
|
|
91
|
-
rows = await fetch
|
|
137
|
+
rows = await _fetch_rows(fetch)
|
|
92
138
|
except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
|
|
93
139
|
unreadable = True
|
|
94
140
|
reasons.append(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
|
|
@@ -107,6 +153,10 @@ async def resolve_agent(name: str, client=None) -> AgentManifest:
|
|
|
107
153
|
"not in ~/.agentos/agents/, your backend agents, or your department defaults"
|
|
108
154
|
)
|
|
109
155
|
if unreadable:
|
|
156
|
+
manifest = await _gateway_manifest(client, name)
|
|
157
|
+
if manifest is not None:
|
|
158
|
+
logger.info("agent lookup fell back to the ML gateway listing for %r", name)
|
|
159
|
+
return manifest
|
|
110
160
|
# Do NOT say "no agent named …" here: a source went unread, so absence was
|
|
111
161
|
# never established. Claiming it sends the user hunting for a typo when the
|
|
112
162
|
# fix is a token or a network.
|
|
@@ -121,9 +121,19 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
121
121
|
# `logger.info` line regardless of whether it buffers or POSTs.
|
|
122
122
|
_ACTIVITY_FLUSH_MIN_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MIN_S", "") or 180.0)
|
|
123
123
|
_ACTIVITY_FLUSH_MAX_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MAX_S", "") or 300.0)
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
124
|
+
# The gateway rejects an `activity` over 2000 characters with a 400 ("Activity
|
|
125
|
+
# must be shorter than or equal to 2000 characters"). The cap used to be 8000, so
|
|
126
|
+
# every coalesced flush and every long scheduled-run summary was refused and the
|
|
127
|
+
# run left no trace in agent_activities. Keep the tail: the most recent lines are
|
|
128
|
+
# the most useful.
|
|
129
|
+
_ACTIVITY_MAX_CHARS = 2000
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fit_activity(text: str) -> str:
|
|
133
|
+
"""``text`` cut to the gateway's limit, keeping the tail behind a ``…`` marker."""
|
|
134
|
+
if len(text) <= _ACTIVITY_MAX_CHARS:
|
|
135
|
+
return text
|
|
136
|
+
return "…\n" + text[-(_ACTIVITY_MAX_CHARS - 2):]
|
|
127
137
|
|
|
128
138
|
|
|
129
139
|
class ActivityFlusher:
|
|
@@ -167,9 +177,7 @@ class ActivityFlusher:
|
|
|
167
177
|
if not self._buf:
|
|
168
178
|
return # nothing pending → don't open a connection at all
|
|
169
179
|
lines, self._buf = self._buf, [] # swap out atomically (no await between)
|
|
170
|
-
body = "\n".join(lines)
|
|
171
|
-
if len(body) > _ACTIVITY_FLUSH_MAX_CHARS:
|
|
172
|
-
body = "…\n" + body[-_ACTIVITY_FLUSH_MAX_CHARS:]
|
|
180
|
+
body = _fit_activity("\n".join(lines))
|
|
173
181
|
try:
|
|
174
182
|
await self._client.log_activity(body, task_id=self._task_id, immediate=True)
|
|
175
183
|
except Exception as exc: # noqa: BLE001 — never let a flush break a run
|
|
@@ -313,6 +321,29 @@ class BackendClient:
|
|
|
313
321
|
except Exception as exc:
|
|
314
322
|
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
315
323
|
|
|
324
|
+
async def assigned_vps_ip(self) -> str:
|
|
325
|
+
"""The IP of the caller's assigned VPS from ``GET /ml-api/users/profile``.
|
|
326
|
+
|
|
327
|
+
``""`` when the profile names no VPS. Raises BackendUnreachable /
|
|
328
|
+
BackendAuthFailed — the caller (``sdk.vps_identity``) turns those into
|
|
329
|
+
"unknown" rather than "not a VPS".
|
|
330
|
+
"""
|
|
331
|
+
from sdk.config import BE_DEFAULT_URL
|
|
332
|
+
from sdk.vps_identity import extract_vps_ip
|
|
333
|
+
|
|
334
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/users/profile"
|
|
335
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
336
|
+
try:
|
|
337
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
338
|
+
resp = await client.get(url, headers=headers)
|
|
339
|
+
_check_status(resp.status_code, url)
|
|
340
|
+
data = (resp.json() or {}).get("data") or {}
|
|
341
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
342
|
+
raise
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
raise BackendUnreachable(url, str(exc)) from exc
|
|
345
|
+
return extract_vps_ip(data.get("user") or {})
|
|
346
|
+
|
|
316
347
|
async def profile(self) -> UserProfile:
|
|
317
348
|
"""GET {BE}/ml-api/users/profile — the caller's role + department.
|
|
318
349
|
|
|
@@ -1766,7 +1797,7 @@ class BackendClient:
|
|
|
1766
1797
|
"developer-name": "null",
|
|
1767
1798
|
"product": "",
|
|
1768
1799
|
}
|
|
1769
|
-
body: dict[str, str] = {"activity": str(activity)}
|
|
1800
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity))}
|
|
1770
1801
|
tid = str(task_id or "").strip()
|
|
1771
1802
|
if tid:
|
|
1772
1803
|
body["taskId"] = tid
|
|
@@ -1800,7 +1831,7 @@ class BackendClient:
|
|
|
1800
1831
|
return False
|
|
1801
1832
|
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agent-activity"
|
|
1802
1833
|
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1803
|
-
body: dict[str, str] = {"activity": str(activity), "agentId": aid}
|
|
1834
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity)), "agentId": aid}
|
|
1804
1835
|
tid = str(task_id or "").strip()
|
|
1805
1836
|
if tid:
|
|
1806
1837
|
body["taskId"] = tid
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""A Jira ticket per scheduled run: open it, mark it In Progress, close it on proof.
|
|
2
|
+
|
|
3
|
+
Opt-in per schedule with a ``ticket`` block on the schedule record::
|
|
4
|
+
|
|
5
|
+
"ticket": {"project_key": "DBNF", "type": "Task", "priority": "Low"}
|
|
6
|
+
|
|
7
|
+
Lifecycle, driven by ``sdk.scheduler.fire``:
|
|
8
|
+
|
|
9
|
+
1. ``open_ticket`` — POST /tasks-ml, then PUT status "In Progress".
|
|
10
|
+
2. the agent runs exactly as it would without a ticket.
|
|
11
|
+
3. ``close_ticket`` — "Done" only when the run succeeded AND its output file
|
|
12
|
+
exists and was written during this run (``evidence_path``); otherwise
|
|
13
|
+
"backlog" with the reason appended to the description.
|
|
14
|
+
|
|
15
|
+
Uses the ML gateway's /tasks-ml directly (``create_task_ml`` / ``update_task_ml``),
|
|
16
|
+
the same calls the Epic decomposition already relies on, so it works without any
|
|
17
|
+
backend deploy. Every function here is best-effort and never raises: a ticket is
|
|
18
|
+
bookkeeping, and losing a nightly run because Jira was slow would be the wrong
|
|
19
|
+
trade. The lowercase "backlog" is deliberate — the gateway validates against
|
|
20
|
+
``{To Do, In Progress, Done, backlog}`` and 400s on "Backlog".
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("sdk.schedule_tickets")
|
|
30
|
+
|
|
31
|
+
DONE = "Done"
|
|
32
|
+
IN_PROGRESS = "In Progress"
|
|
33
|
+
BACKLOG = "backlog"
|
|
34
|
+
_MAX_REASON = 500
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class RunTicket:
|
|
39
|
+
task_id: str
|
|
40
|
+
key: str
|
|
41
|
+
description: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def to_record(ticket: RunTicket | None) -> dict | None:
|
|
45
|
+
"""Plain dict to persist on the schedule while a run is being retried."""
|
|
46
|
+
if ticket is None:
|
|
47
|
+
return None
|
|
48
|
+
return {"task_id": ticket.task_id, "key": ticket.key, "description": ticket.description}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def from_record(record) -> RunTicket | None:
|
|
52
|
+
"""The ticket a previous attempt of this run opened, if one was persisted."""
|
|
53
|
+
if not isinstance(record, dict) or not record.get("task_id"):
|
|
54
|
+
return None
|
|
55
|
+
return RunTicket(task_id=str(record["task_id"]), key=str(record.get("key") or ""),
|
|
56
|
+
description=str(record.get("description") or ""))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def enabled(schedule: dict) -> bool:
|
|
60
|
+
cfg = schedule.get("ticket")
|
|
61
|
+
return isinstance(cfg, dict) and bool(str(cfg.get("project_key") or "").strip())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _summary(schedule: dict, when: datetime) -> str:
|
|
65
|
+
variables = schedule.get("variables") or {}
|
|
66
|
+
target = variables.get("repo") or ""
|
|
67
|
+
branch = variables.get("branch") or ""
|
|
68
|
+
if target and branch:
|
|
69
|
+
target = f"{target}@{branch}"
|
|
70
|
+
parts = ["[Scheduled]", str(schedule.get("agent") or "agent")]
|
|
71
|
+
if target:
|
|
72
|
+
parts.append(f"— {target}")
|
|
73
|
+
parts.append(f"— {when:%Y-%m-%d}")
|
|
74
|
+
return " ".join(parts)[:255]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _description(schedule: dict, expected: str) -> str:
|
|
78
|
+
variables = schedule.get("variables") or {}
|
|
79
|
+
lines = [
|
|
80
|
+
f"Automated run of agent '{schedule.get('agent')}' by the AgentOS scheduler.",
|
|
81
|
+
f"Schedule: {schedule.get('cron') or schedule.get('run_at')} ({schedule.get('timezone')}), "
|
|
82
|
+
f"id {schedule.get('schedule_id')}.",
|
|
83
|
+
]
|
|
84
|
+
if expected:
|
|
85
|
+
lines.append(f"Output: {expected}")
|
|
86
|
+
shown = {k: v for k, v in variables.items() if k in ("repo", "repo_path", "branch", "output_file")}
|
|
87
|
+
if shown:
|
|
88
|
+
lines.append("Inputs: " + ", ".join(f"{k}={v}" for k, v in shown.items()))
|
|
89
|
+
lines.append("Closed as Done only when the run succeeds and its output file is written.")
|
|
90
|
+
return "\n".join(lines)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def expected_output(schedule: dict) -> str:
|
|
94
|
+
"""The file (or folder) this run is expected to write, for the ticket text."""
|
|
95
|
+
variables = schedule.get("variables") or {}
|
|
96
|
+
return str(variables.get("output_file") or schedule.get("output_dir") or "")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def evidence_path(schedule: dict, record: Path | None, delivered_to: str,
|
|
100
|
+
started_at: datetime) -> tuple[Path | None, str]:
|
|
101
|
+
"""``(path, "")`` when the run's output exists and is fresh, else ``(None, why)``.
|
|
102
|
+
|
|
103
|
+
The agent's own ``output_file`` variable is the strongest proof (the per-repo
|
|
104
|
+
suggestion and quality files). Failing that, the run record copied into the
|
|
105
|
+
schedule's export folder (the AI Research Agent's daily file). Failing that,
|
|
106
|
+
the run record itself.
|
|
107
|
+
"""
|
|
108
|
+
variables = schedule.get("variables") or {}
|
|
109
|
+
want = str(variables.get("output_file") or "").strip()
|
|
110
|
+
if want:
|
|
111
|
+
candidate = Path(want).expanduser()
|
|
112
|
+
elif delivered_to and record is not None:
|
|
113
|
+
candidate = Path(delivered_to) / Path(record).name
|
|
114
|
+
elif record is not None:
|
|
115
|
+
candidate = Path(record)
|
|
116
|
+
else:
|
|
117
|
+
return None, "the run produced no output file"
|
|
118
|
+
try:
|
|
119
|
+
stat = candidate.stat()
|
|
120
|
+
except OSError:
|
|
121
|
+
return None, f"output file was not created: {candidate}"
|
|
122
|
+
written = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc)
|
|
123
|
+
if written < started_at.replace(microsecond=0):
|
|
124
|
+
return None, f"output file was not updated by this run: {candidate}"
|
|
125
|
+
return candidate, ""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
async def _owner_email(client) -> str:
|
|
129
|
+
try:
|
|
130
|
+
return str((await client.profile()).email or "").strip()
|
|
131
|
+
except Exception: # noqa: BLE001
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def open_ticket(client, schedule: dict, when: datetime | None = None) -> RunTicket | None:
|
|
136
|
+
"""Create the run's ticket and move it to In Progress. ``None`` on any failure."""
|
|
137
|
+
if not enabled(schedule):
|
|
138
|
+
return None
|
|
139
|
+
cfg = schedule["ticket"]
|
|
140
|
+
when = when or datetime.now(timezone.utc)
|
|
141
|
+
description = _description(schedule, expected_output(schedule))
|
|
142
|
+
fields = {
|
|
143
|
+
"projectKey": str(cfg["project_key"]).strip(),
|
|
144
|
+
"type": str(cfg.get("type") or "Task"),
|
|
145
|
+
"priority": str(cfg.get("priority") or "Low"),
|
|
146
|
+
"summary": _summary(schedule, when),
|
|
147
|
+
"description": description,
|
|
148
|
+
}
|
|
149
|
+
email = await _owner_email(client)
|
|
150
|
+
if email:
|
|
151
|
+
fields["assigneeEmail"] = email
|
|
152
|
+
try:
|
|
153
|
+
task_id = await client.create_task_ml(fields)
|
|
154
|
+
except Exception as exc: # noqa: BLE001
|
|
155
|
+
logger.warning("schedule_tickets.create_failed sid=%s err=%s", schedule.get("schedule_id"), exc)
|
|
156
|
+
return None
|
|
157
|
+
if not task_id:
|
|
158
|
+
logger.warning("schedule_tickets.create_failed sid=%s detail=%s",
|
|
159
|
+
schedule.get("schedule_id"), getattr(client, "last_task_ml_error", ""))
|
|
160
|
+
return None
|
|
161
|
+
key = ""
|
|
162
|
+
try:
|
|
163
|
+
record = await client.get_task_ml(task_id) or {}
|
|
164
|
+
key = str(record.get("jiraIssueKey") or record.get("jiraKey") or "")
|
|
165
|
+
except Exception: # noqa: BLE001 — the key is for display only
|
|
166
|
+
pass
|
|
167
|
+
ticket = RunTicket(task_id=str(task_id), key=key, description=description)
|
|
168
|
+
await _set_status(client, ticket, IN_PROGRESS)
|
|
169
|
+
return ticket
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
async def _set_status(client, ticket: RunTicket, status: str, extra: dict | None = None) -> bool:
|
|
173
|
+
try:
|
|
174
|
+
ok = await client.update_task_ml(ticket.task_id, {"status": status, **(extra or {})})
|
|
175
|
+
except Exception as exc: # noqa: BLE001
|
|
176
|
+
logger.warning("schedule_tickets.status_failed task=%s status=%s err=%s",
|
|
177
|
+
ticket.task_id, status, exc)
|
|
178
|
+
return False
|
|
179
|
+
if not ok:
|
|
180
|
+
logger.warning("schedule_tickets.status_failed task=%s status=%s detail=%s",
|
|
181
|
+
ticket.task_id, status, getattr(client, "last_task_ml_error", ""))
|
|
182
|
+
return bool(ok)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
async def close_ticket(client, ticket: RunTicket | None, *, ok: bool, reason: str = "",
|
|
186
|
+
output: Path | None = None) -> str:
|
|
187
|
+
"""Done on proven success, backlog otherwise. Returns the status set ("" if none)."""
|
|
188
|
+
if ticket is None:
|
|
189
|
+
return ""
|
|
190
|
+
if ok and output is not None:
|
|
191
|
+
done = await _set_status(client, ticket, DONE,
|
|
192
|
+
{"description": f"{ticket.description}\nResult: {output}"})
|
|
193
|
+
return DONE if done else ""
|
|
194
|
+
why = (reason or "the run did not complete").strip()[:_MAX_REASON]
|
|
195
|
+
moved = await _set_status(client, ticket, BACKLOG,
|
|
196
|
+
{"description": f"{ticket.description}\nFailed: {why}"})
|
|
197
|
+
return BACKLOG if moved else ""
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def label(ticket: RunTicket | None) -> str:
|
|
201
|
+
return (ticket.key or ticket.task_id) if ticket else ""
|