devops-bot-sdk 1.6.66__tar.gz → 1.6.69__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.66 → devops_bot_sdk-1.6.69}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/SOURCES.txt +4 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/chat.py +51 -1
- devops_bot_sdk-1.6.69/sdk/schedule_tickets.py +201 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/scheduler.py +119 -15
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/service.py +94 -22
- devops_bot_sdk-1.6.69/tests/test_schedule_tickets.py +218 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_scheduler.py +36 -0
- devops_bot_sdk-1.6.69/tests/test_scheduler_transient_retry.py +230 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_update_timer.py +2 -0
- devops_bot_sdk-1.6.69/tests/test_windows_task_paths.py +172 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/README.md +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/os_setup.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/shipper.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.66 → devops_bot_sdk-1.6.69}/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
|
|
@@ -203,7 +204,9 @@ tests/test_run_outputs.py
|
|
|
203
204
|
tests/test_run_verdict_is_honest.py
|
|
204
205
|
tests/test_schedule_api.py
|
|
205
206
|
tests/test_schedule_outputs_and_plans.py
|
|
207
|
+
tests/test_schedule_tickets.py
|
|
206
208
|
tests/test_scheduler.py
|
|
209
|
+
tests/test_scheduler_transient_retry.py
|
|
207
210
|
tests/test_self_report.py
|
|
208
211
|
tests/test_service_token_warning.py
|
|
209
212
|
tests/test_stale_panel_restart.py
|
|
@@ -224,4 +227,5 @@ tests/test_updater_reports_the_truth.py
|
|
|
224
227
|
tests/test_url_access.py
|
|
225
228
|
tests/test_vps_health.py
|
|
226
229
|
tests/test_windows_loop_identity.py
|
|
230
|
+
tests/test_windows_task_paths.py
|
|
227
231
|
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.69
|
|
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.69" # 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
|
|
@@ -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.
|
|
@@ -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 ""
|
|
@@ -56,6 +56,17 @@ _MAX_JITTER_S = 60.0
|
|
|
56
56
|
|
|
57
57
|
_semaphore: asyncio.Semaphore | None = None
|
|
58
58
|
_running: set[str] = set() # schedule_ids currently in flight, this process
|
|
59
|
+
# schedule_ids dispatched and not yet finished — including ones still WAITING for
|
|
60
|
+
# the semaphore. mark_fired only runs once fire() starts, so without this a due
|
|
61
|
+
# schedule queued behind a long run was re-dispatched on every tick and then ran
|
|
62
|
+
# once per queued copy.
|
|
63
|
+
_queued: set[str] = set()
|
|
64
|
+
|
|
65
|
+
# A run whose agent could not be LOOKED UP (backend unreadable) is re-armed and
|
|
66
|
+
# retried instead of being lost for the day: this many times, this far apart.
|
|
67
|
+
# 3 x 5 min stays well inside the default 1 h catch-up grace window.
|
|
68
|
+
MAX_TRANSIENT_RETRIES = 3
|
|
69
|
+
TRANSIENT_RETRY_DELAY_S = 300.0
|
|
59
70
|
|
|
60
71
|
|
|
61
72
|
def _setting(name: str) -> str:
|
|
@@ -202,10 +213,15 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
202
213
|
return out
|
|
203
214
|
|
|
204
215
|
_running.add(sid)
|
|
216
|
+
# Kept so a transient lookup failure can re-arm the schedule (see below).
|
|
217
|
+
previous_fire = schedule.get("last_fire_at")
|
|
218
|
+
rearmed = False
|
|
205
219
|
# Stamped BEFORE the run, not after: a long run, or one queued behind the
|
|
206
220
|
# semaphore, would otherwise still look due on the next tick.
|
|
207
221
|
schedules.mark_fired(sid)
|
|
208
222
|
started = time.monotonic()
|
|
223
|
+
run_started = datetime.now(timezone.utc)
|
|
224
|
+
ticket = None # sdk.schedule_tickets.RunTicket when the schedule opts in
|
|
209
225
|
|
|
210
226
|
try:
|
|
211
227
|
if schedule.get("kind") == "plan":
|
|
@@ -222,9 +238,16 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
222
238
|
out["status"] = "inactive"
|
|
223
239
|
return out
|
|
224
240
|
|
|
241
|
+
from sdk import schedule_tickets
|
|
225
242
|
from sdk.agents import activity
|
|
226
243
|
from sdk.agents.chat import AgentLookupUnavailable, run_agent
|
|
227
244
|
|
|
245
|
+
# Opened only once the agent is known to be runnable, so a disabled agent
|
|
246
|
+
# never leaves a ticket behind. Best-effort: no ticket never blocks a run.
|
|
247
|
+
# A retry of the same run reuses the ticket its first attempt opened.
|
|
248
|
+
ticket = (schedule_tickets.from_record(schedule.get("open_ticket"))
|
|
249
|
+
or await schedule_tickets.open_ticket(client, schedule, run_started))
|
|
250
|
+
|
|
228
251
|
trigger = f"schedule · {schedule.get('cron')} ({schedule.get('timezone')})"
|
|
229
252
|
try:
|
|
230
253
|
result = await run_agent(
|
|
@@ -247,18 +270,46 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
247
270
|
# auto-disable — a few flaky minutes would otherwise switch off every
|
|
248
271
|
# schedule on the box.
|
|
249
272
|
transient = isinstance(exc, AgentLookupUnavailable)
|
|
273
|
+
attempt = int(schedule.get("transient_attempts") or 0) + 1
|
|
274
|
+
if transient and attempt <= MAX_TRANSIENT_RETRIES:
|
|
275
|
+
# The backend could not be READ: nothing ran. Put last_fire_at back
|
|
276
|
+
# so the schedule is due again, and hold it off for a few minutes,
|
|
277
|
+
# keeping the ticket open for the retry to reuse.
|
|
278
|
+
retry_at = datetime.now(timezone.utc).timestamp() + TRANSIENT_RETRY_DELAY_S
|
|
279
|
+
schedules.update(
|
|
280
|
+
sid, last_fire_at=previous_fire, transient_attempts=attempt,
|
|
281
|
+
retry_after=datetime.fromtimestamp(retry_at, timezone.utc)
|
|
282
|
+
.isoformat(timespec="seconds"),
|
|
283
|
+
open_ticket=schedule_tickets.to_record(ticket),
|
|
284
|
+
)
|
|
285
|
+
rearmed = True
|
|
286
|
+
schedules.mark_result(
|
|
287
|
+
sid, ok=True,
|
|
288
|
+
status=_with_ticket(
|
|
289
|
+
f"retrying {attempt}/{MAX_TRANSIENT_RETRIES} in "
|
|
290
|
+
f"{int(TRANSIENT_RETRY_DELAY_S // 60)} min — {exc}", ticket, ""),
|
|
291
|
+
)
|
|
292
|
+
logger.warning("scheduler.lookup_retry agent=%s attempt=%s err=%s",
|
|
293
|
+
agent, attempt, exc)
|
|
294
|
+
out["status"] = "retrying"
|
|
295
|
+
return out
|
|
250
296
|
result = {"ok": False, "error": str(exc), "result": "",
|
|
251
297
|
"input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0,
|
|
252
298
|
"files_changed": []}
|
|
253
299
|
path = run_log.start_run(agent, kind="scheduled", trigger=trigger,
|
|
254
300
|
scope=str(schedule.get("scope") or "custom"))
|
|
255
301
|
run_log.finish_run(path, result)
|
|
302
|
+
await schedule_tickets.close_ticket(client, ticket, ok=False, reason=str(exc))
|
|
256
303
|
schedules.mark_result(
|
|
257
304
|
sid,
|
|
258
305
|
ok=transient, # transient → no strike, but still reported
|
|
259
|
-
status=(
|
|
306
|
+
status=_with_ticket(
|
|
307
|
+
f"gave up after {MAX_TRANSIENT_RETRIES} retries — {exc}" if transient
|
|
308
|
+
else f"failed — {exc}", ticket, "backlog"),
|
|
260
309
|
output_file=path.name if path else "",
|
|
261
|
-
|
|
310
|
+
# A failure record is never exported: the schedule's folder is
|
|
311
|
+
# where the person reads RESULTS (e.g. one research report a day).
|
|
312
|
+
delivered_to="",
|
|
262
313
|
)
|
|
263
314
|
logger.warning("scheduler.run_unrunnable agent=%s transient=%s err=%s",
|
|
264
315
|
agent, transient, exc)
|
|
@@ -268,15 +319,29 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
268
319
|
elapsed = time.monotonic() - started
|
|
269
320
|
ok = bool(result.get("ok"))
|
|
270
321
|
latest = run_log.list_runs(agent, limit=1)
|
|
322
|
+
record = latest[0] if latest else None
|
|
323
|
+
# Only a successful run is exported; a failure stays in the agent's own
|
|
324
|
+
# run history, so the export folder never holds an error as a "report".
|
|
325
|
+
delivered_to = schedules.deliver(
|
|
326
|
+
schedule, record,
|
|
327
|
+
list(result.get("files_changed") or []),
|
|
328
|
+
workspace=_agent_workspace(agent),
|
|
329
|
+
) if ok else ""
|
|
330
|
+
ticket_status = ""
|
|
331
|
+
if ticket is not None:
|
|
332
|
+
output, missing = schedule_tickets.evidence_path(
|
|
333
|
+
schedule, record, delivered_to, run_started)
|
|
334
|
+
reason = (str(result.get("error") or "the agent reported a failure")
|
|
335
|
+
if not ok else missing)
|
|
336
|
+
ticket_status = await schedule_tickets.close_ticket(
|
|
337
|
+
client, ticket, ok=ok and output is not None, reason=reason, output=output)
|
|
271
338
|
schedules.mark_result(
|
|
272
339
|
sid, ok=ok,
|
|
273
|
-
status=
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
workspace=_agent_workspace(agent),
|
|
279
|
-
),
|
|
340
|
+
status=_with_ticket(
|
|
341
|
+
"ok" if ok else f"failed — {str(result.get('error') or '')[:200]}",
|
|
342
|
+
ticket, ticket_status),
|
|
343
|
+
output_file=record.name if record else "",
|
|
344
|
+
delivered_to=delivered_to,
|
|
280
345
|
)
|
|
281
346
|
# One call writes BOTH the local usage ledger and the backend
|
|
282
347
|
# agent_activities feed, and never raises.
|
|
@@ -287,10 +352,19 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
287
352
|
|
|
288
353
|
except Exception as exc: # noqa: BLE001 — a bad run must never end the loop
|
|
289
354
|
logger.warning("scheduler.fire_failed agent=%s err=%s", agent, exc)
|
|
290
|
-
|
|
355
|
+
if ticket is not None:
|
|
356
|
+
try:
|
|
357
|
+
from sdk import schedule_tickets
|
|
358
|
+
await schedule_tickets.close_ticket(client, ticket, ok=False, reason=str(exc))
|
|
359
|
+
except Exception: # noqa: BLE001 — never let ticket bookkeeping escape
|
|
360
|
+
pass
|
|
361
|
+
schedules.mark_result(sid, ok=False, status=_with_ticket(f"failed — {exc}", ticket, "backlog"))
|
|
291
362
|
out["status"] = "failed"
|
|
292
363
|
return out
|
|
293
364
|
finally:
|
|
365
|
+
if not rearmed and any(schedule.get(k) for k in ("transient_attempts", "retry_after", "open_ticket")):
|
|
366
|
+
# The run reached a real outcome: clear any retry state it carried.
|
|
367
|
+
schedules.update(sid, transient_attempts=0, retry_after="", open_ticket=None)
|
|
294
368
|
_running.discard(sid)
|
|
295
369
|
release_lock(agent)
|
|
296
370
|
try:
|
|
@@ -299,6 +373,14 @@ async def fire(schedule: dict, *, client=None) -> dict:
|
|
|
299
373
|
pass
|
|
300
374
|
|
|
301
375
|
|
|
376
|
+
def _with_ticket(status: str, ticket, ticket_status: str) -> str:
|
|
377
|
+
"""Append the run's ticket to the schedule's status line, when it has one."""
|
|
378
|
+
if ticket is None:
|
|
379
|
+
return status
|
|
380
|
+
key = ticket.key or ticket.task_id
|
|
381
|
+
return f"{status} · ticket {key}" + (f" → {ticket_status}" if ticket_status else "")
|
|
382
|
+
|
|
383
|
+
|
|
302
384
|
def _agent_workspace(agent: str) -> Path | None:
|
|
303
385
|
"""The directory the agent ran in — where any file it wrote lives."""
|
|
304
386
|
try:
|
|
@@ -371,9 +453,13 @@ async def _fire_guarded(schedule: dict) -> None:
|
|
|
371
453
|
global _semaphore
|
|
372
454
|
if _semaphore is None:
|
|
373
455
|
_semaphore = asyncio.Semaphore(concurrency())
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
456
|
+
sid = str(schedule.get("schedule_id") or "")
|
|
457
|
+
try:
|
|
458
|
+
async with _semaphore:
|
|
459
|
+
await asyncio.sleep(random.uniform(0, _MAX_JITTER_S))
|
|
460
|
+
await fire(schedule)
|
|
461
|
+
finally:
|
|
462
|
+
_queued.discard(sid)
|
|
377
463
|
|
|
378
464
|
|
|
379
465
|
# ── The loop ────────────────────────────────────────────────────────────────
|
|
@@ -383,13 +469,30 @@ def due_now(now: datetime | None = None) -> list[dict]:
|
|
|
383
469
|
edit in the panel takes effect on the next tick with no restart."""
|
|
384
470
|
now = now or datetime.now(timezone.utc)
|
|
385
471
|
grace = grace_s()
|
|
386
|
-
return [s for s in schedules.list_schedules()
|
|
472
|
+
return [s for s in schedules.list_schedules()
|
|
473
|
+
if schedules.is_due(s, now, grace) and not _held_off(s, now)]
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _held_off(schedule: dict, now: datetime) -> bool:
|
|
477
|
+
"""True while a re-armed schedule is waiting out its retry delay."""
|
|
478
|
+
raw = schedule.get("retry_after")
|
|
479
|
+
if not raw:
|
|
480
|
+
return False
|
|
481
|
+
try:
|
|
482
|
+
until = datetime.fromisoformat(str(raw))
|
|
483
|
+
except ValueError:
|
|
484
|
+
return False
|
|
485
|
+
if until.tzinfo is None:
|
|
486
|
+
until = until.replace(tzinfo=timezone.utc)
|
|
487
|
+
return now < until
|
|
387
488
|
|
|
388
489
|
|
|
389
490
|
async def tick(now: datetime | None = None) -> int:
|
|
390
491
|
"""One evaluation pass. Returns how many runs were dispatched."""
|
|
391
|
-
ready = due_now(now)
|
|
492
|
+
ready = [s for s in due_now(now)
|
|
493
|
+
if str(s.get("schedule_id") or "") not in _queued]
|
|
392
494
|
for schedule in ready:
|
|
495
|
+
_queued.add(str(schedule.get("schedule_id") or ""))
|
|
393
496
|
asyncio.ensure_future(_fire_guarded(schedule))
|
|
394
497
|
return len(ready)
|
|
395
498
|
|
|
@@ -429,6 +532,7 @@ def status() -> dict:
|
|
|
429
532
|
"total": len(rows),
|
|
430
533
|
"active": sum(1 for s in rows if s.get("enabled", True)),
|
|
431
534
|
"in_flight": sorted(_running),
|
|
535
|
+
"queued": sorted(_queued - _running),
|
|
432
536
|
"next": [
|
|
433
537
|
{
|
|
434
538
|
"schedule_id": s.get("schedule_id"),
|