devops-bot-sdk 1.6.33__tar.gz → 1.6.37__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.33 → devops_bot_sdk-1.6.37}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/SOURCES.txt +26 -3
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/entry_points.txt +1 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/pyproject.toml +1 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agentd.py +21 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/cli.py +31 -1
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/client.py +243 -55
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/config.py +105 -1
- devops_bot_sdk-1.6.37/sdk/estimator.py +246 -0
- devops_bot_sdk-1.6.37/sdk/firewall.py +310 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/formats.py +28 -2
- devops_bot_sdk-1.6.37/sdk/github_ingest.py +573 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/graph_recon.py +137 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/graphify.py +309 -21
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/intake.py +154 -9
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/handlers.py +422 -15
- devops_bot_sdk-1.6.37/sdk/journal.py +388 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/local_exec.py +259 -50
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/plan_run.py +26 -4
- devops_bot_sdk-1.6.37/sdk/prd_progress.py +288 -0
- devops_bot_sdk-1.6.37/sdk/providers.py +631 -0
- devops_bot_sdk-1.6.37/sdk/resume.py +383 -0
- devops_bot_sdk-1.6.37/sdk/router.py +420 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/run_auto.py +17 -24
- devops_bot_sdk-1.6.37/sdk/swagger.py +356 -0
- devops_bot_sdk-1.6.37/sdk/task_types.py +199 -0
- devops_bot_sdk-1.6.37/sdk/tiers/__init__.py +17 -0
- devops_bot_sdk-1.6.37/sdk/tiers/mini_agent.py +336 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/api.py +84 -28
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/ip_allowlist.py +43 -16
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/models.py +15 -14
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/server.py +125 -17
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/app.js +87 -10
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/chat.html +27 -8
- devops_bot_sdk-1.6.37/sdk/vps_health.py +264 -0
- devops_bot_sdk-1.6.37/sdk/write_hook.py +242 -0
- devops_bot_sdk-1.6.37/tests/test_cheap_tier.py +428 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_client_jira_projects.py +45 -4
- devops_bot_sdk-1.6.37/tests/test_estimator.py +285 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_formats.py +56 -5
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_github_ingest.py +8 -4
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graphify_build.py +134 -4
- devops_bot_sdk-1.6.37/tests/test_journal.py +288 -0
- devops_bot_sdk-1.6.37/tests/test_local_exec_provider.py +295 -0
- devops_bot_sdk-1.6.37/tests/test_prd_artifact.py +473 -0
- devops_bot_sdk-1.6.37/tests/test_providers.py +378 -0
- devops_bot_sdk-1.6.37/tests/test_resume.py +495 -0
- devops_bot_sdk-1.6.37/tests/test_router.py +447 -0
- devops_bot_sdk-1.6.37/tests/test_swagger_command.py +260 -0
- devops_bot_sdk-1.6.37/tests/test_tier_flag_parity.py +364 -0
- devops_bot_sdk-1.6.37/tests/test_ui_gate.py +281 -0
- devops_bot_sdk-1.6.37/tests/test_ui_lifespan.py +89 -0
- devops_bot_sdk-1.6.37/tests/test_write_hook.py +289 -0
- devops_bot_sdk-1.6.33/sdk/github_ingest.py +0 -161
- devops_bot_sdk-1.6.33/sdk/ui/static/login.html +0 -111
- devops_bot_sdk-1.6.33/sdk/vps_health.py +0 -111
- devops_bot_sdk-1.6.33/tests/test_otp_login.py +0 -178
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/README.md +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.33 → devops_bot_sdk-1.6.37}/tests/test_vps_health.py +0 -0
|
@@ -14,31 +14,41 @@ sdk/client.py
|
|
|
14
14
|
sdk/config.py
|
|
15
15
|
sdk/crucial.py
|
|
16
16
|
sdk/departments.py
|
|
17
|
+
sdk/estimator.py
|
|
17
18
|
sdk/exceptions.py
|
|
19
|
+
sdk/firewall.py
|
|
18
20
|
sdk/formats.py
|
|
19
21
|
sdk/git_ops.py
|
|
20
22
|
sdk/github_ingest.py
|
|
21
23
|
sdk/graph_recon.py
|
|
22
24
|
sdk/graphify.py
|
|
23
25
|
sdk/intake.py
|
|
26
|
+
sdk/journal.py
|
|
24
27
|
sdk/local_exec.py
|
|
25
28
|
sdk/phase_timer.py
|
|
26
29
|
sdk/plan_run.py
|
|
27
30
|
sdk/platform_compat.py
|
|
31
|
+
sdk/prd_progress.py
|
|
28
32
|
sdk/proof_readers.py
|
|
33
|
+
sdk/providers.py
|
|
29
34
|
sdk/py.typed
|
|
35
|
+
sdk/resume.py
|
|
36
|
+
sdk/router.py
|
|
30
37
|
sdk/run_auto.py
|
|
31
38
|
sdk/scheduler.py
|
|
32
39
|
sdk/self_report.py
|
|
33
40
|
sdk/self_report_local.py
|
|
34
41
|
sdk/sse.py
|
|
42
|
+
sdk/swagger.py
|
|
35
43
|
sdk/task_file.py
|
|
44
|
+
sdk/task_types.py
|
|
36
45
|
sdk/test.py
|
|
37
46
|
sdk/test_pipeline.py
|
|
38
47
|
sdk/ticket_fields.py
|
|
39
48
|
sdk/updater.py
|
|
40
49
|
sdk/url_access.py
|
|
41
50
|
sdk/vps_health.py
|
|
51
|
+
sdk/write_hook.py
|
|
42
52
|
sdk/agents/__init__.py
|
|
43
53
|
sdk/agents/activity.py
|
|
44
54
|
sdk/agents/builder.py
|
|
@@ -83,6 +93,8 @@ sdk/models/envelope.py
|
|
|
83
93
|
sdk/models/requests.py
|
|
84
94
|
sdk/models/responses.py
|
|
85
95
|
sdk/models/snapshots.py
|
|
96
|
+
sdk/tiers/__init__.py
|
|
97
|
+
sdk/tiers/mini_agent.py
|
|
86
98
|
sdk/ui/__init__.py
|
|
87
99
|
sdk/ui/api.py
|
|
88
100
|
sdk/ui/dashboard.py
|
|
@@ -102,7 +114,6 @@ sdk/ui/static/chat.html
|
|
|
102
114
|
sdk/ui/static/custom-agents.html
|
|
103
115
|
sdk/ui/static/default-agents.html
|
|
104
116
|
sdk/ui/static/index.html
|
|
105
|
-
sdk/ui/static/login.html
|
|
106
117
|
sdk/ui/static/report.html
|
|
107
118
|
sdk/ui/static/runs.html
|
|
108
119
|
sdk/ui/static/schedules.html
|
|
@@ -122,6 +133,7 @@ tests/test_agents_connectors_builder.py
|
|
|
122
133
|
tests/test_allowed_tools_config.py
|
|
123
134
|
tests/test_auto_agent_dispatch.py
|
|
124
135
|
tests/test_browser_profile.py
|
|
136
|
+
tests/test_cheap_tier.py
|
|
125
137
|
tests/test_client_jira_projects.py
|
|
126
138
|
tests/test_config_token_diagnostics.py
|
|
127
139
|
tests/test_configure_token_owner.py
|
|
@@ -130,6 +142,7 @@ tests/test_departments.py
|
|
|
130
142
|
tests/test_epic_decompose_run.py
|
|
131
143
|
tests/test_epic_inherited_agent_id.py
|
|
132
144
|
tests/test_epic_session_isolation.py
|
|
145
|
+
tests/test_estimator.py
|
|
133
146
|
tests/test_formats.py
|
|
134
147
|
tests/test_git_action.py
|
|
135
148
|
tests/test_github_ingest.py
|
|
@@ -138,24 +151,34 @@ tests/test_graphify_build.py
|
|
|
138
151
|
tests/test_graphify_hook_install.py
|
|
139
152
|
tests/test_health.py
|
|
140
153
|
tests/test_intake_plan_review.py
|
|
141
|
-
tests/
|
|
154
|
+
tests/test_journal.py
|
|
155
|
+
tests/test_local_exec_provider.py
|
|
142
156
|
tests/test_phase_timer.py
|
|
143
157
|
tests/test_plan_new_command.py
|
|
144
158
|
tests/test_plan_run_activity.py
|
|
159
|
+
tests/test_prd_artifact.py
|
|
145
160
|
tests/test_proof_readers.py
|
|
161
|
+
tests/test_providers.py
|
|
146
162
|
tests/test_recon_session_isolation.py
|
|
163
|
+
tests/test_resume.py
|
|
164
|
+
tests/test_router.py
|
|
147
165
|
tests/test_run_auto_agent_only.py
|
|
148
166
|
tests/test_run_outputs.py
|
|
149
167
|
tests/test_schedule_api.py
|
|
150
168
|
tests/test_scheduler.py
|
|
151
169
|
tests/test_self_report.py
|
|
152
170
|
tests/test_service_token_warning.py
|
|
171
|
+
tests/test_swagger_command.py
|
|
153
172
|
tests/test_task_file_and_plan_run.py
|
|
154
173
|
tests/test_task_file_format_doc.py
|
|
155
174
|
tests/test_ticket_fields.py
|
|
156
175
|
tests/test_ticket_run_resolve_by_id.py
|
|
157
176
|
tests/test_ticket_speed_fixes.py
|
|
177
|
+
tests/test_tier_flag_parity.py
|
|
178
|
+
tests/test_ui_gate.py
|
|
179
|
+
tests/test_ui_lifespan.py
|
|
158
180
|
tests/test_update_timer.py
|
|
159
181
|
tests/test_updater_extras.py
|
|
160
182
|
tests/test_url_access.py
|
|
161
|
-
tests/test_vps_health.py
|
|
183
|
+
tests/test_vps_health.py
|
|
184
|
+
tests/test_write_hook.py
|
|
@@ -54,6 +54,7 @@ agentos = "sdk.cli:main" # unified CLI: configure/agent/integ
|
|
|
54
54
|
agentos-auto = "sdk.run_auto:main" # run orchestrate-auto LOCALLY (co_ token from config)
|
|
55
55
|
agentos-agentd = "sdk.agentd:main" # long-running custom-agent daemon
|
|
56
56
|
agentos-ui = "sdk.ui.server:main" # local web control panel (needs the [ui] extra)
|
|
57
|
+
agentos-swagger = "sdk.swagger:main" # where /docs is + can you reach it (= `agentos swagger`)
|
|
57
58
|
|
|
58
59
|
[project.urls]
|
|
59
60
|
Homepage = "https://agentos.io"
|
|
@@ -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.37
|
|
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.37" # 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
|
|
@@ -429,8 +429,25 @@ async def _dispatch_agent_run(
|
|
|
429
429
|
)
|
|
430
430
|
run_log.log_line(_log_path, f"task_id={task_id}")
|
|
431
431
|
|
|
432
|
+
# A custom-agent run writes real files, so it gets the SAME shared
|
|
433
|
+
# post-write hook the coding path uses: journal (synchronous,
|
|
434
|
+
# survives a kill) → requirement overlay → graph refresh. Without
|
|
435
|
+
# this, a custom-agent run produced no graph update and no journal
|
|
436
|
+
# entry at all, so a tier switch during one had no anchor to resume
|
|
437
|
+
# from — the run type was simply outside the resume triple.
|
|
438
|
+
from sdk import write_hook
|
|
439
|
+
from sdk.agents.engines.claude_code import ensure_agent_workspace
|
|
440
|
+
|
|
441
|
+
_workspace = str(ensure_agent_workspace(manifest.name))
|
|
442
|
+
_record_write = write_hook.make_recorder(
|
|
443
|
+
task_id=task_id or f"agent-{manifest.name}",
|
|
444
|
+
repo_path=_workspace,
|
|
445
|
+
)
|
|
446
|
+
_write_events = write_hook.make_event_hook(_record_write)
|
|
447
|
+
|
|
432
448
|
async def _tee(ev: dict) -> None:
|
|
433
449
|
run_log.log_event(_log_path, ev)
|
|
450
|
+
await _write_events(ev)
|
|
434
451
|
|
|
435
452
|
_started = time.monotonic()
|
|
436
453
|
local_result = await claude_code_engine.run(
|
|
@@ -438,6 +455,10 @@ async def _dispatch_agent_run(
|
|
|
438
455
|
on_event=_tee,
|
|
439
456
|
)
|
|
440
457
|
run_log.finish_run(_log_path, local_result)
|
|
458
|
+
# Backstop for writes the stream did not surface (a resumed turn,
|
|
459
|
+
# an engine that tees events elsewhere). Ordering is lost, but an
|
|
460
|
+
# ordered-but-empty journal is worse than an unordered complete one.
|
|
461
|
+
await write_hook.record_result_files(_record_write, local_result)
|
|
441
462
|
# Local ledger + platform `agent_activities` feed. EVERY trigger path
|
|
442
463
|
# reaches this function (cron schedule, ticket label, on-demand
|
|
443
464
|
# run-on-ticket from the UI / CLI / desktop IPC), so a VPS agent run is
|
|
@@ -58,6 +58,26 @@ Command surface:
|
|
|
58
58
|
agentos integration rm <name>
|
|
59
59
|
Remove a registered integration.
|
|
60
60
|
|
|
61
|
+
agentos firewall [--apply] [--revoke IP] [--port N]
|
|
62
|
+
Print the host-firewall rules that open the agentos-ui port to every IP in
|
|
63
|
+
the application allowlist (`sdk.config.get_allowed_ips`), for THIS OS — ufw
|
|
64
|
+
on Linux, `netsh advfirewall` on Windows, a pf note on macOS. Prints by
|
|
65
|
+
default; `--apply` executes them and needs root / Administrator. The host
|
|
66
|
+
firewall is a separate layer the allowlist cannot reach, and the two drifting
|
|
67
|
+
apart is what silently blinded every VPS when the backend's egress IP moved.
|
|
68
|
+
See sdk.firewall.
|
|
69
|
+
|
|
70
|
+
agentos swagger [--open] [--json [PATH]] [--port N]
|
|
71
|
+
Where the panel's Swagger UI is and whether it can actually be reached:
|
|
72
|
+
the URL derived from the bind address the service was installed with,
|
|
73
|
+
whether a co_ token is configured (masked), how many operations the spec
|
|
74
|
+
has, and a ufw/netsh diff against sdk.config.get_allowed_ips when the HOST
|
|
75
|
+
firewall would drop the port — a layer the SDK's own allowlist cannot see.
|
|
76
|
+
`--open` launches a browser; `--json` writes openapi.json from
|
|
77
|
+
create_app().openapi() with no server running (`--json -` pipes it).
|
|
78
|
+
Also flags routes registered twice, since the second copy is dead code.
|
|
79
|
+
Needs the [ui] extra for the route count and --json. See sdk.swagger.
|
|
80
|
+
|
|
61
81
|
agentos report submit [--date YYYY-MM-DD] (--text "…" | --file PATH)
|
|
62
82
|
[--project NAME] [--proof PATH|URL]… [--no-wait]
|
|
63
83
|
Report work that never reached Jira or GitHub, attach proofs (local
|
|
@@ -139,6 +159,14 @@ def main() -> None:
|
|
|
139
159
|
_github_command(rest)
|
|
140
160
|
return
|
|
141
161
|
|
|
162
|
+
if noun == "firewall":
|
|
163
|
+
from sdk import firewall
|
|
164
|
+
sys.exit(firewall.cli(rest))
|
|
165
|
+
|
|
166
|
+
if noun == "swagger":
|
|
167
|
+
from sdk import swagger
|
|
168
|
+
sys.exit(swagger.cli(rest))
|
|
169
|
+
|
|
142
170
|
if noun == "report":
|
|
143
171
|
_report_command(rest)
|
|
144
172
|
return
|
|
@@ -152,7 +180,7 @@ def main() -> None:
|
|
|
152
180
|
|
|
153
181
|
|
|
154
182
|
def _print_usage() -> None:
|
|
155
|
-
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser> ...")
|
|
183
|
+
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|firewall|swagger> ...")
|
|
156
184
|
print()
|
|
157
185
|
print(" agentos configure [--rotate]")
|
|
158
186
|
print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
|
|
@@ -164,6 +192,8 @@ def _print_usage() -> None:
|
|
|
164
192
|
print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
|
|
165
193
|
print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
|
|
166
194
|
print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
|
|
195
|
+
print(" agentos firewall [--apply] [--revoke IP] [--port N] # open the UI port to the allowlisted IPs")
|
|
196
|
+
print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
|
|
167
197
|
|
|
168
198
|
|
|
169
199
|
# ── agent ────────────────────────────────────────────────────────────────
|
|
@@ -45,6 +45,13 @@ logger = logging.getLogger(__name__)
|
|
|
45
45
|
# agents without a lookup on every run.
|
|
46
46
|
_AGENT_ID_CACHE: dict[str, str] = {}
|
|
47
47
|
|
|
48
|
+
# GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
|
|
49
|
+
# monotonic). Keyed per credential so two clients on one box never share a token.
|
|
50
|
+
# Without this the token is re-fetched on EVERY call, which is what
|
|
51
|
+
# tests/test_ticket_speed_fixes.py::TestGithubTokenCache guards against.
|
|
52
|
+
_GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
|
|
53
|
+
_GITHUB_TOKEN_TTL_S = float(os.getenv("AGENTOS_GITHUB_TOKEN_TTL_S", "") or 1800.0)
|
|
54
|
+
|
|
48
55
|
# GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
|
|
49
56
|
# monotonic). Keyed per credential so two clients on one box never share a token.
|
|
50
57
|
_GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
|
|
@@ -197,6 +204,30 @@ _ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS exec
|
|
|
197
204
|
_AUTO_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — same budget for multi-task auto runs
|
|
198
205
|
|
|
199
206
|
|
|
207
|
+
def _jira_project_records(payload: dict) -> list[dict]:
|
|
208
|
+
"""Normalise a jira-projects envelope to `list[dict]`.
|
|
209
|
+
|
|
210
|
+
The endpoint returns `data` as a FLAT LIST. Sibling endpoints on the same
|
|
211
|
+
gateway wrap theirs in `data.records` / `data.record`, and this shape is not
|
|
212
|
+
documented, so all three are tolerated rather than guessing one — a wrong
|
|
213
|
+
guess reads as "no projects", which is indistinguishable from a real empty
|
|
214
|
+
result and would silently disable the whole picker.
|
|
215
|
+
"""
|
|
216
|
+
data = payload.get("data")
|
|
217
|
+
if isinstance(data, list):
|
|
218
|
+
return [r for r in data if isinstance(r, dict)]
|
|
219
|
+
if isinstance(data, dict):
|
|
220
|
+
records = data.get("records")
|
|
221
|
+
if isinstance(records, list):
|
|
222
|
+
return [r for r in records if isinstance(r, dict)]
|
|
223
|
+
record = data.get("record")
|
|
224
|
+
if isinstance(record, dict):
|
|
225
|
+
return [record]
|
|
226
|
+
if data.get("id") or data.get("jiraProjectId"):
|
|
227
|
+
return [dict(data)]
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
|
|
200
231
|
class BackendClient:
|
|
201
232
|
"""Thin HTTPS/SSE client — wraps all backend /api/v1/* endpoints.
|
|
202
233
|
|
|
@@ -928,51 +959,71 @@ class BackendClient:
|
|
|
928
959
|
co_ token. Best-effort: returns None on any failure so a run without a
|
|
929
960
|
connected GitHub account still proceeds (clone/PR steps just won't auth).
|
|
930
961
|
"""
|
|
962
|
+
import time as _time
|
|
963
|
+
|
|
964
|
+
now = _time.monotonic()
|
|
965
|
+
cached = _GITHUB_TOKEN_CACHE.get(self._token)
|
|
966
|
+
if cached and now < cached[1]:
|
|
967
|
+
return cached[0]
|
|
931
968
|
record = await self.github_token_record()
|
|
932
|
-
|
|
969
|
+
token = (record or {}).get("accessToken") or None
|
|
970
|
+
if token:
|
|
971
|
+
_GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
|
|
972
|
+
return token
|
|
933
973
|
|
|
934
|
-
async def ingest_github_prs(self, since: str | None = None) -> dict:
|
|
974
|
+
async def ingest_github_prs(self, since: str | None = None) -> dict | None:
|
|
935
975
|
"""POST {AI}/api/v1/github/prs — trigger this user's daily PR ingestion.
|
|
936
976
|
|
|
937
|
-
The
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
977
|
+
The daily trigger. The work runs on the AI backend (LLM summarise, the
|
|
978
|
+
downstream daily-summary forward, the Qdrant upsert); the SDK only decides
|
|
979
|
+
WHEN, because only this machine knows its own catch-up watermark.
|
|
980
|
+
|
|
981
|
+
The backend resolves the caller's GitHub link server-side from the co_ token
|
|
982
|
+
in ``Authorization`` and OVERWRITES whatever the body sends
|
|
983
|
+
(``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md`` §7/§9 — that endpoint used to
|
|
984
|
+
take a GitHub access token in an unauthenticated body). So this deliberately
|
|
985
|
+
sends NO credentials: a client that still supplied them would be shipping a
|
|
986
|
+
secret the server ignores.
|
|
987
|
+
|
|
988
|
+
``since`` is an ISO date (``YYYY-MM-DD``) → the backend's ``date_from``.
|
|
989
|
+
Omitted means "today", which is what a never-missed run wants; the cron sends
|
|
990
|
+
the watermark instead so a machine that was off does not lose days.
|
|
991
|
+
|
|
992
|
+
Returns the response JSON (``{status, job_id, status_url, ...}``) or **None**
|
|
993
|
+
on any failure — never raises into the cron. WHY it failed lands in
|
|
994
|
+
``self.last_ingest_error`` (``{status, body, retryable}``), which the
|
|
995
|
+
scheduler needs to tell a busy backend (retry in 30 min) from a rejected
|
|
996
|
+
request (wait for the next slot).
|
|
946
997
|
"""
|
|
947
|
-
|
|
998
|
+
payload = [{
|
|
948
999
|
"userId": self._user_id_from_token(),
|
|
949
|
-
"
|
|
950
|
-
"githubAccessToken": "",
|
|
951
|
-
"apiTokenHash": self._token,
|
|
952
|
-
"date_from": since,
|
|
1000
|
+
"apiTokenHash": self._token, # downstream daily-summary Authorization
|
|
953
1001
|
}]
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1002
|
+
if since:
|
|
1003
|
+
payload[0]["date_from"] = since
|
|
1004
|
+
self.last_ingest_error = {}
|
|
1005
|
+
try:
|
|
1006
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
1007
|
+
resp = await client.post(
|
|
1008
|
+
self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
|
|
1009
|
+
)
|
|
1010
|
+
if resp.status_code >= 400:
|
|
1011
|
+
body = resp.text[:500]
|
|
1012
|
+
logger.warning("client.ingest_github_prs failed status=%s body=%s",
|
|
1013
|
+
resp.status_code, body[:200])
|
|
1014
|
+
self.last_ingest_error = {
|
|
1015
|
+
"status": resp.status_code,
|
|
1016
|
+
"body": body,
|
|
1017
|
+
"retryable": is_retryable_backend_error(resp.status_code, body),
|
|
1018
|
+
}
|
|
1019
|
+
return None
|
|
1020
|
+
return resp.json()
|
|
1021
|
+
except Exception as exc: # noqa: BLE001 — a cron must not die on a blip
|
|
1022
|
+
logger.warning("client.ingest_github_prs error err=%s", exc)
|
|
1023
|
+
# A transport failure (timeout, reset, DNS blip) is as retryable as an
|
|
1024
|
+
# exhausted pool — the backend may simply be busy.
|
|
1025
|
+
self.last_ingest_error = {"status": 0, "body": str(exc)[:500], "retryable": True}
|
|
1026
|
+
return None
|
|
976
1027
|
|
|
977
1028
|
async def move_to_backlog(self, task_id: str) -> bool:
|
|
978
1029
|
"""PUT {BE}/tasks-ml/{id} status=backlog (best-effort).
|
|
@@ -1096,6 +1147,108 @@ class BackendClient:
|
|
|
1096
1147
|
logger.warning("client.set_task_status error task=%s err=%s", task_id, exc)
|
|
1097
1148
|
return False
|
|
1098
1149
|
|
|
1150
|
+
# ── Cheap tier (T0) — the backend holds the Azure/GCP credentials ───
|
|
1151
|
+
|
|
1152
|
+
async def llm_complete(
|
|
1153
|
+
self,
|
|
1154
|
+
messages: list[dict],
|
|
1155
|
+
*,
|
|
1156
|
+
tools: list[dict] | None = None,
|
|
1157
|
+
step_type: str = "small_task",
|
|
1158
|
+
task_id: str | None = None,
|
|
1159
|
+
budget_tokens: int | None = None,
|
|
1160
|
+
used_tokens: int = 0,
|
|
1161
|
+
max_tokens: int | None = None,
|
|
1162
|
+
timeout: float = 120.0,
|
|
1163
|
+
attempts: int = 3,
|
|
1164
|
+
) -> dict:
|
|
1165
|
+
"""POST /api/v1/llm/complete — one turn of the cheap-tier tool loop.
|
|
1166
|
+
|
|
1167
|
+
The credential asymmetry is deliberate (plan §6): Azure/GCP keys stay
|
|
1168
|
+
**server-side** because the backend is the client for T0, while the
|
|
1169
|
+
Bedrock/subscription credentials stay desktop-side because the local
|
|
1170
|
+
``claude`` CLI is the client for T1/T2. Each tier's creds live where that
|
|
1171
|
+
tier's client runs, so the desktop never holds an Azure key.
|
|
1172
|
+
|
|
1173
|
+
The budget is enforced **server-side** — the desktop is untrusted for
|
|
1174
|
+
accounting — and a breach comes back as ``409 budget_exhausted``, which
|
|
1175
|
+
the router treats as "escalate to T1", not as an error.
|
|
1176
|
+
|
|
1177
|
+
Returns ``{"ok", "text", "tool_calls", "model", "tokens_in",
|
|
1178
|
+
"tokens_out", "status", "reason"}``. Never raises: an unreachable
|
|
1179
|
+
backend is a *routing signal* (the cheap tier is the LESS available one),
|
|
1180
|
+
so it comes back as ``ok=False`` with ``reason="backend_unreachable"``
|
|
1181
|
+
and the router escalates.
|
|
1182
|
+
|
|
1183
|
+
Retries only what is worth retrying, via the existing
|
|
1184
|
+
``is_retryable_backend_error`` — so a saturated Postgres pool surfaced
|
|
1185
|
+
as a 5xx is retried rather than escalating the whole task off the cheap
|
|
1186
|
+
tier for a transient blip.
|
|
1187
|
+
"""
|
|
1188
|
+
path = "/api/v1/llm/complete"
|
|
1189
|
+
payload: dict[str, Any] = {
|
|
1190
|
+
"messages": messages,
|
|
1191
|
+
"step_type": step_type,
|
|
1192
|
+
}
|
|
1193
|
+
if tools:
|
|
1194
|
+
payload["tools"] = tools
|
|
1195
|
+
if task_id:
|
|
1196
|
+
payload["task_id"] = task_id
|
|
1197
|
+
if budget_tokens is not None:
|
|
1198
|
+
payload["budget_tokens"] = budget_tokens
|
|
1199
|
+
if used_tokens:
|
|
1200
|
+
payload["used_tokens"] = used_tokens
|
|
1201
|
+
if max_tokens is not None:
|
|
1202
|
+
payload["max_tokens"] = max_tokens
|
|
1203
|
+
|
|
1204
|
+
def _fail(reason: str, status: int = 0, detail: str = "") -> dict:
|
|
1205
|
+
return {"ok": False, "text": "", "tool_calls": [], "model": None,
|
|
1206
|
+
"tokens_in": 0, "tokens_out": 0, "status": status,
|
|
1207
|
+
"reason": reason, "detail": detail[:300]}
|
|
1208
|
+
|
|
1209
|
+
last = _fail("backend_unreachable")
|
|
1210
|
+
for attempt in range(max(1, attempts)):
|
|
1211
|
+
try:
|
|
1212
|
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
|
1213
|
+
resp = await client.post(
|
|
1214
|
+
self._url(path), json=payload, headers=self._headers,
|
|
1215
|
+
)
|
|
1216
|
+
except Exception as exc: # noqa: BLE001 — network/DNS/TLS
|
|
1217
|
+
last = _fail("backend_unreachable", detail=str(exc))
|
|
1218
|
+
if attempt + 1 < attempts:
|
|
1219
|
+
await asyncio.sleep(min(2.0 * (attempt + 1), 5.0))
|
|
1220
|
+
continue
|
|
1221
|
+
return last
|
|
1222
|
+
body = resp.text or ""
|
|
1223
|
+
if resp.status_code == 409:
|
|
1224
|
+
# The hard ceiling. One mechanism serves both accounting and the
|
|
1225
|
+
# cap, so there is nothing to reconcile between them.
|
|
1226
|
+
return _fail("budget_exhausted", resp.status_code, body)
|
|
1227
|
+
if resp.status_code >= 400:
|
|
1228
|
+
if is_retryable_backend_error(resp.status_code, body) and attempt + 1 < attempts:
|
|
1229
|
+
last = _fail("retryable", resp.status_code, body)
|
|
1230
|
+
await asyncio.sleep(min(2.0 * (attempt + 1), 5.0))
|
|
1231
|
+
continue
|
|
1232
|
+
reason = ("auth" if resp.status_code == 401
|
|
1233
|
+
else "unavailable" if resp.status_code >= 500
|
|
1234
|
+
else "rejected")
|
|
1235
|
+
return _fail(reason, resp.status_code, body)
|
|
1236
|
+
try:
|
|
1237
|
+
data = resp.json() or {}
|
|
1238
|
+
except Exception: # noqa: BLE001 — a 200 with a non-JSON body
|
|
1239
|
+
return _fail("bad_response", resp.status_code, body)
|
|
1240
|
+
return {
|
|
1241
|
+
"ok": True,
|
|
1242
|
+
"text": str(data.get("text") or ""),
|
|
1243
|
+
"tool_calls": list(data.get("tool_calls") or []),
|
|
1244
|
+
"model": data.get("model"),
|
|
1245
|
+
"tokens_in": int(data.get("tokens_in") or 0),
|
|
1246
|
+
"tokens_out": int(data.get("tokens_out") or 0),
|
|
1247
|
+
"status": resp.status_code,
|
|
1248
|
+
"reason": "",
|
|
1249
|
+
}
|
|
1250
|
+
return last
|
|
1251
|
+
|
|
1099
1252
|
async def update_task_ml(
|
|
1100
1253
|
self, task_id: str, fields: dict, model_used: list[str] | None = None,
|
|
1101
1254
|
) -> bool:
|
|
@@ -1214,15 +1367,32 @@ class BackendClient:
|
|
|
1214
1367
|
return None
|
|
1215
1368
|
|
|
1216
1369
|
async def list_jira_projects(
|
|
1217
|
-
self, pv_id: str, limit: int = 100, offset: int = 0,
|
|
1370
|
+
self, pv_id: str = "", limit: int = 100, offset: int = 0,
|
|
1371
|
+
*, all_: bool = False,
|
|
1218
1372
|
) -> list[dict]:
|
|
1219
|
-
"""GET {BE}/ml-api/jira-projects
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1373
|
+
"""GET {BE}/ml-api/jira-projects — project records.
|
|
1374
|
+
|
|
1375
|
+
Two fixes over the original, both of which made an unfiltered listing
|
|
1376
|
+
impossible:
|
|
1377
|
+
|
|
1378
|
+
1. `pvId` is now OMITTED when blank. It used to be sent unconditionally as
|
|
1379
|
+
`pvId=""`, which scopes the query to nothing, so there was no way to
|
|
1380
|
+
ask "every project I can see". That matters because `pvId` is null on
|
|
1381
|
+
the large majority of live rows (57 projects, 3 with a pvId at time of
|
|
1382
|
+
writing) — a pvId-only client can reach almost none of the boards a
|
|
1383
|
+
person would actually name.
|
|
1384
|
+
2. The envelope is read TOLERANTLY. This endpoint returns `data` as a
|
|
1385
|
+
FLAT LIST; the original read `data.records` only, so an unfiltered call
|
|
1386
|
+
would have returned `[]` even once (1) was fixed. Both shapes are now
|
|
1387
|
+
accepted, plus a bare `data.record`.
|
|
1388
|
+
|
|
1389
|
+
Pass `all_=True` to send the gateway's `all=true` (no pagination) instead
|
|
1390
|
+
of `limit`/`offset`. Match on `key` to resolve an existing project before
|
|
1391
|
+
falling back to `create_jira_project` — `key` is the same string a ticket
|
|
1392
|
+
carries as `projectKey`.
|
|
1393
|
+
|
|
1394
|
+
Returns [] on any failure, logged rather than silent, so a broken gateway
|
|
1395
|
+
cannot read as "there are no projects".
|
|
1226
1396
|
"""
|
|
1227
1397
|
from sdk.config import BE_DEFAULT_URL
|
|
1228
1398
|
|
|
@@ -1233,25 +1403,43 @@ class BackendClient:
|
|
|
1233
1403
|
"developer-name": "null",
|
|
1234
1404
|
"product": "",
|
|
1235
1405
|
}
|
|
1406
|
+
params: dict[str, object] = {"offset": offset}
|
|
1407
|
+
if all_:
|
|
1408
|
+
params["all"] = "true"
|
|
1409
|
+
else:
|
|
1410
|
+
params["limit"] = limit
|
|
1411
|
+
# Only scope by product when there is one — see (1) above.
|
|
1412
|
+
if str(pv_id or "").strip():
|
|
1413
|
+
params["pvId"] = str(pv_id).strip()
|
|
1236
1414
|
try:
|
|
1237
|
-
# All three params are always sent — the wire URL keeps its documented
|
|
1238
|
-
# shape even when the caller has no pvId to scope by.
|
|
1239
1415
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
1240
|
-
resp = await client.get(
|
|
1241
|
-
url,
|
|
1242
|
-
params={"limit": limit, "offset": offset, "pvId": str(pv_id or "")},
|
|
1243
|
-
headers=headers,
|
|
1244
|
-
)
|
|
1416
|
+
resp = await client.get(url, params=params, headers=headers)
|
|
1245
1417
|
if resp.status_code >= 400:
|
|
1246
1418
|
logger.warning("client.list_jira_projects failed status=%s body=%s",
|
|
1247
1419
|
resp.status_code, resp.text[:300])
|
|
1248
1420
|
return []
|
|
1249
|
-
|
|
1250
|
-
return records if isinstance(records, list) else []
|
|
1421
|
+
return _jira_project_records(resp.json() or {})
|
|
1251
1422
|
except Exception as exc: # noqa: BLE001
|
|
1252
1423
|
logger.warning("client.list_jira_projects error err=%s", exc)
|
|
1253
1424
|
return []
|
|
1254
1425
|
|
|
1426
|
+
async def find_jira_project_by_key(self, key: str) -> dict | None:
|
|
1427
|
+
"""The project whose `key` matches, case-insensitively, or None.
|
|
1428
|
+
|
|
1429
|
+
The verification step before filing tickets: a key that no longer resolves
|
|
1430
|
+
means the board was renamed or deleted, and creating N tickets against it
|
|
1431
|
+
would fail N times with nothing naming the cause. Matched locally over the
|
|
1432
|
+
full list because the gateway exposes no `?key=` filter.
|
|
1433
|
+
"""
|
|
1434
|
+
wanted = str(key or "").strip().casefold()
|
|
1435
|
+
if not wanted:
|
|
1436
|
+
return None
|
|
1437
|
+
for record in await self.list_jira_projects(all_=True):
|
|
1438
|
+
candidate = record.get("key")
|
|
1439
|
+
if isinstance(candidate, str) and candidate.strip().casefold() == wanted:
|
|
1440
|
+
return record
|
|
1441
|
+
return None
|
|
1442
|
+
|
|
1255
1443
|
async def add_comment(self, task_id: str, body: str) -> bool:
|
|
1256
1444
|
"""POST /tasks-ml/{id}/comments — add a comment to a ticket (best-effort).
|
|
1257
1445
|
|