devops-bot-sdk 1.6.19__tar.gz → 1.6.24__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.19 → devops_bot_sdk-1.6.24}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/SOURCES.txt +16 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agentd.py +36 -1
- devops_bot_sdk-1.6.24/sdk/agents/activity.py +250 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/run_log.py +12 -1
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/cli.py +19 -1
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/client.py +122 -7
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/config.py +11 -0
- devops_bot_sdk-1.6.24/sdk/graph_recon.py +330 -0
- devops_bot_sdk-1.6.24/sdk/graphify.py +475 -0
- devops_bot_sdk-1.6.24/sdk/intake.py +236 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ipc/handlers.py +605 -133
- devops_bot_sdk-1.6.24/sdk/phase_timer.py +192 -0
- devops_bot_sdk-1.6.24/sdk/plan_run.py +287 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/run_auto.py +165 -5
- devops_bot_sdk-1.6.24/sdk/task_file.py +221 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/api.py +273 -34
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/models.py +65 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/app.js +496 -1
- devops_bot_sdk-1.6.24/sdk/ui/static/chat.html +154 -0
- devops_bot_sdk-1.6.24/sdk/ui/static/runs.html +79 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/styles.css +92 -0
- devops_bot_sdk-1.6.24/tests/test_agent_activity_tracking.py +449 -0
- devops_bot_sdk-1.6.24/tests/test_epic_decompose_run.py +173 -0
- devops_bot_sdk-1.6.24/tests/test_epic_session_isolation.py +100 -0
- devops_bot_sdk-1.6.24/tests/test_graph_recon.py +245 -0
- devops_bot_sdk-1.6.24/tests/test_graphify_build.py +274 -0
- devops_bot_sdk-1.6.24/tests/test_graphify_hook_install.py +187 -0
- devops_bot_sdk-1.6.24/tests/test_phase_timer.py +139 -0
- devops_bot_sdk-1.6.24/tests/test_recon_session_isolation.py +122 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_run_auto_agent_only.py +9 -0
- devops_bot_sdk-1.6.24/tests/test_task_file_and_plan_run.py +392 -0
- devops_bot_sdk-1.6.24/tests/test_ticket_speed_fixes.py +318 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_update_timer.py +45 -1
- devops_bot_sdk-1.6.19/sdk/agents/activity.py +0 -117
- devops_bot_sdk-1.6.19/sdk/graphify.py +0 -172
- devops_bot_sdk-1.6.19/sdk/ui/static/runs.html +0 -42
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/README.md +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.19 → devops_bot_sdk-1.6.24}/tests/test_vps_health.py +0 -0
|
@@ -17,13 +17,18 @@ sdk/exceptions.py
|
|
|
17
17
|
sdk/formats.py
|
|
18
18
|
sdk/git_ops.py
|
|
19
19
|
sdk/github_ingest.py
|
|
20
|
+
sdk/graph_recon.py
|
|
20
21
|
sdk/graphify.py
|
|
22
|
+
sdk/intake.py
|
|
21
23
|
sdk/local_exec.py
|
|
24
|
+
sdk/phase_timer.py
|
|
25
|
+
sdk/plan_run.py
|
|
22
26
|
sdk/platform_compat.py
|
|
23
27
|
sdk/py.typed
|
|
24
28
|
sdk/run_auto.py
|
|
25
29
|
sdk/scheduler.py
|
|
26
30
|
sdk/sse.py
|
|
31
|
+
sdk/task_file.py
|
|
27
32
|
sdk/test.py
|
|
28
33
|
sdk/test_pipeline.py
|
|
29
34
|
sdk/ticket_fields.py
|
|
@@ -88,6 +93,7 @@ sdk/ui/session.py
|
|
|
88
93
|
sdk/ui/setup_manager.py
|
|
89
94
|
sdk/ui/static/agents.html
|
|
90
95
|
sdk/ui/static/app.js
|
|
96
|
+
sdk/ui/static/chat.html
|
|
91
97
|
sdk/ui/static/custom-agents.html
|
|
92
98
|
sdk/ui/static/default-agents.html
|
|
93
99
|
sdk/ui/static/index.html
|
|
@@ -98,6 +104,7 @@ sdk/ui/static/setup.html
|
|
|
98
104
|
sdk/ui/static/styles.css
|
|
99
105
|
sdk/ui/static/tickets.html
|
|
100
106
|
tests/test_agent_activity.py
|
|
107
|
+
tests/test_agent_activity_tracking.py
|
|
101
108
|
tests/test_agent_chat_features.py
|
|
102
109
|
tests/test_agent_listing.py
|
|
103
110
|
tests/test_agent_prompt_async.py
|
|
@@ -113,19 +120,28 @@ tests/test_config_token_diagnostics.py
|
|
|
113
120
|
tests/test_configure_token_owner.py
|
|
114
121
|
tests/test_console_encoding.py
|
|
115
122
|
tests/test_departments.py
|
|
123
|
+
tests/test_epic_decompose_run.py
|
|
116
124
|
tests/test_epic_inherited_agent_id.py
|
|
125
|
+
tests/test_epic_session_isolation.py
|
|
117
126
|
tests/test_formats.py
|
|
118
127
|
tests/test_git_action.py
|
|
119
128
|
tests/test_github_ingest.py
|
|
129
|
+
tests/test_graph_recon.py
|
|
130
|
+
tests/test_graphify_build.py
|
|
131
|
+
tests/test_graphify_hook_install.py
|
|
120
132
|
tests/test_health.py
|
|
121
133
|
tests/test_otp_login.py
|
|
134
|
+
tests/test_phase_timer.py
|
|
135
|
+
tests/test_recon_session_isolation.py
|
|
122
136
|
tests/test_run_auto_agent_only.py
|
|
123
137
|
tests/test_run_outputs.py
|
|
124
138
|
tests/test_schedule_api.py
|
|
125
139
|
tests/test_scheduler.py
|
|
126
140
|
tests/test_service_token_warning.py
|
|
141
|
+
tests/test_task_file_and_plan_run.py
|
|
127
142
|
tests/test_ticket_fields.py
|
|
128
143
|
tests/test_ticket_run_resolve_by_id.py
|
|
144
|
+
tests/test_ticket_speed_fixes.py
|
|
129
145
|
tests/test_update_timer.py
|
|
130
146
|
tests/test_updater_extras.py
|
|
131
147
|
tests/test_vps_health.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.24
|
|
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.24" # 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
|
|
@@ -114,6 +114,22 @@ def _stop_requested() -> bool:
|
|
|
114
114
|
return _STOP_FILE.exists()
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def _auto_loop_owns_queue() -> bool:
|
|
118
|
+
"""True when an `agentos-auto` loop is live and processing the ticket queue.
|
|
119
|
+
|
|
120
|
+
Reuses run_auto's own liveness check, which already distinguishes a running
|
|
121
|
+
daemon from a stale PID file left by a crash. Best-effort: if we cannot tell,
|
|
122
|
+
say False — refusing to dispatch on an unreadable PID file would silently
|
|
123
|
+
disable ticket-label agents, which is worse than the overlap we are avoiding.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
from sdk import run_auto
|
|
127
|
+
|
|
128
|
+
return run_auto._daemon_running() is not None
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
117
133
|
def _set_stop() -> None:
|
|
118
134
|
_AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
|
|
119
135
|
_STOP_FILE.write_text("stop")
|
|
@@ -585,11 +601,30 @@ class AgentDaemon:
|
|
|
585
601
|
`_TICKET_POLL_INTERVAL_S` between iterations. Keeps an in-memory set
|
|
586
602
|
of already-dispatched task ids so a task is never re-fired on a later
|
|
587
603
|
poll within this daemon's lifetime.
|
|
604
|
+
|
|
605
|
+
DEFERS while an `agentos-auto` loop owns the queue. That loop processes
|
|
606
|
+
tickets strictly sequentially, oldest `createdAt` first; dispatching from
|
|
607
|
+
here concurrently (`asyncio.create_task` below) would run a ticket
|
|
608
|
+
alongside it and out of order — two agents editing the same working
|
|
609
|
+
directory at once, and a queue that no longer honours creation time.
|
|
610
|
+
Deferring costs at most one poll interval: the ticket stays To Do and is
|
|
611
|
+
picked up on a later tick, or by the auto-loop itself.
|
|
588
612
|
"""
|
|
589
613
|
dispatched_task_ids: set[str] = set()
|
|
590
614
|
ticket_agents = [m for m in self._manifests if m.trigger.type == "ticket_label"]
|
|
615
|
+
deferring = False
|
|
591
616
|
|
|
592
617
|
while not _stop_requested():
|
|
618
|
+
if ticket_agents and _auto_loop_owns_queue():
|
|
619
|
+
if not deferring:
|
|
620
|
+
logger.info("agentd.watch_tickets_deferred reason=auto_loop_running")
|
|
621
|
+
deferring = True
|
|
622
|
+
await asyncio.sleep(_TICKET_POLL_INTERVAL_S)
|
|
623
|
+
continue
|
|
624
|
+
if deferring:
|
|
625
|
+
logger.info("agentd.watch_tickets_resumed")
|
|
626
|
+
deferring = False
|
|
627
|
+
|
|
593
628
|
if ticket_agents:
|
|
594
629
|
try:
|
|
595
630
|
tasks = await self.client.list_todo_tasks_raw()
|
|
@@ -611,7 +646,7 @@ class AgentDaemon:
|
|
|
611
646
|
|
|
612
647
|
await asyncio.sleep(_TICKET_POLL_INTERVAL_S)
|
|
613
648
|
|
|
614
|
-
async def run_forever(self) -> None:
|
|
649
|
+
async def run_forever(self) -> None: # noqa: D401 — see class docstring
|
|
615
650
|
"""Load + schedule agents, start the cron scheduler, then watch tickets
|
|
616
651
|
until stopped. Shuts the scheduler down cleanly on the way out."""
|
|
617
652
|
await self.load_and_schedule()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Agent-activity reporting — the ONE place a custom / department agent run is
|
|
2
|
+
recorded, for both sinks:
|
|
3
|
+
|
|
4
|
+
* the LOCAL usage ledger (``sdk.agents.usage`` → ``~/.agentos/usage.jsonl``),
|
|
5
|
+
which backs ``GET /api/ui/agents/usage`` and, through it, the AI backend
|
|
6
|
+
report's "VPS Agent Logs" section, and
|
|
7
|
+
* the PLATFORM activity feed — ``POST {ML_GATEWAY}/ml-api/agent-activity``
|
|
8
|
+
with ``{activity, agentId}``, which the ML backend writes into the Postgres
|
|
9
|
+
``agent_activities`` table (docs/DATABASE_SCHEMA.md #3: ``user_id``,
|
|
10
|
+
``activity``, ``agent_id``) and the AI backend reads back RBAC-scoped for
|
|
11
|
+
reporting + chat.
|
|
12
|
+
|
|
13
|
+
Every VPS-side agent run goes through here no matter how it was triggered — the
|
|
14
|
+
control panel (chat / run / run-on-ticket), an ``agentd`` cron schedule, an
|
|
15
|
+
``agentd`` ticket-label trigger, the CLI, or the desktop IPC bridge. Before this
|
|
16
|
+
module only the control-panel paths emitted, so scheduled and ticket-triggered
|
|
17
|
+
runs never reached ``agent_activities``.
|
|
18
|
+
|
|
19
|
+
``agent_activities.agent_id`` is an FK onto ``agents.id``, so only agents that
|
|
20
|
+
exist in the BACKEND emit — ``BackendClient.resolve_agent_id`` returns None for
|
|
21
|
+
a local-only manifest and the emit is skipped (the local ledger still records
|
|
22
|
+
it). Both custom and department-default agents resolve, so both are tracked.
|
|
23
|
+
|
|
24
|
+
Best-effort throughout: nothing in here raises into a run.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import os
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
34
|
+
from sdk.client import BackendClient
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger("sdk.agents.activity")
|
|
37
|
+
|
|
38
|
+
# How a run reached us → the verb that opens the activity line. Keep these short:
|
|
39
|
+
# the feed is rendered as one line per row in reports and in chat answers.
|
|
40
|
+
_VERB = {
|
|
41
|
+
"chat": "Chatted with",
|
|
42
|
+
"run": "Ran",
|
|
43
|
+
"run-ticket": "Ran",
|
|
44
|
+
"scheduled": "Ran (schedule)",
|
|
45
|
+
"ticket_label": "Ran (ticket trigger)",
|
|
46
|
+
"cli": "Ran (cli)",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
51
|
+
detail: str = "") -> str:
|
|
52
|
+
"""The human-readable ``activity`` string stored in ``agent_activities``."""
|
|
53
|
+
res = result or {}
|
|
54
|
+
verb = _VERB.get(kind, "Ran")
|
|
55
|
+
status = "ok" if res.get("ok") else "error"
|
|
56
|
+
line = f"{verb} agent '{name}' — {status}"
|
|
57
|
+
if duration_s:
|
|
58
|
+
line += f" ({round(float(duration_s), 1)}s)"
|
|
59
|
+
err = str(res.get("error") or "").strip()
|
|
60
|
+
if status == "error" and err:
|
|
61
|
+
line += f": {err[:200]}"
|
|
62
|
+
if detail:
|
|
63
|
+
line += f" — {detail}"
|
|
64
|
+
return line
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Run-log lines → the activity event ────────────────────────────────────
|
|
68
|
+
# Every line `run_log.log_line` writes to a run's Markdown file is also noted
|
|
69
|
+
# here, so the activity event a run produces carries what the run actually DID
|
|
70
|
+
# rather than only that it finished. The buffer exists because the alternative —
|
|
71
|
+
# a webhook POST per line — is exactly the pattern that saturated the shared
|
|
72
|
+
# Postgres pool and led to `client.ActivityFlusher` (see sdk/client.py:100).
|
|
73
|
+
#
|
|
74
|
+
# Two sinks, matching that design:
|
|
75
|
+
# * a ticket-scoped `ActivityFlusher`, when one is active (the ticket loop sets
|
|
76
|
+
# it) — `record()` does no I/O and the flusher coalesces into one POST every
|
|
77
|
+
# 3-5 minutes;
|
|
78
|
+
# * otherwise this per-run buffer, drained by `record_run` into the ONE
|
|
79
|
+
# per-run event. So an agent run costs one POST no matter how chatty it is.
|
|
80
|
+
_LINE_BUF: dict[str, list[str]] = {}
|
|
81
|
+
_LINE_BUF_MAX_LINES = 40 # keep the tail — recent lines are the useful ones
|
|
82
|
+
_LINE_BUF_MAX_CHARS = 4000 # bounded like _ACTIVITY_FLUSH_MAX_CHARS
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def note(bucket: str, text: str) -> None:
|
|
86
|
+
"""Record one run-log line against ``bucket`` (a run's output folder name).
|
|
87
|
+
|
|
88
|
+
Never raises and never does I/O: this is called from `run_log.log_line`, on
|
|
89
|
+
the hot path of a streaming engine run.
|
|
90
|
+
"""
|
|
91
|
+
line = str(text or "").strip()
|
|
92
|
+
if not line:
|
|
93
|
+
return
|
|
94
|
+
try:
|
|
95
|
+
from sdk.client import _ACTIVE_ACTIVITY_FLUSHER
|
|
96
|
+
flusher = _ACTIVE_ACTIVITY_FLUSHER.get()
|
|
97
|
+
except Exception: # noqa: BLE001
|
|
98
|
+
flusher = None
|
|
99
|
+
if flusher is not None:
|
|
100
|
+
flusher.record(line)
|
|
101
|
+
return
|
|
102
|
+
buf = _LINE_BUF.setdefault(str(bucket), [])
|
|
103
|
+
buf.append(line)
|
|
104
|
+
if len(buf) > _LINE_BUF_MAX_LINES:
|
|
105
|
+
del buf[: len(buf) - _LINE_BUF_MAX_LINES]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def drain(bucket: str) -> list[str]:
|
|
109
|
+
"""Take and clear the lines noted for ``bucket``."""
|
|
110
|
+
return _LINE_BUF.pop(str(bucket), [])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _lines_block(lines: list[str]) -> str:
|
|
114
|
+
"""The recorded lines as one bounded block, newest-biased."""
|
|
115
|
+
if not lines:
|
|
116
|
+
return ""
|
|
117
|
+
out: list[str] = []
|
|
118
|
+
total = 0
|
|
119
|
+
for line in reversed(lines): # newest first, then re-reverse
|
|
120
|
+
if total + len(line) + 1 > _LINE_BUF_MAX_CHARS:
|
|
121
|
+
break
|
|
122
|
+
out.append(line)
|
|
123
|
+
total += len(line) + 1
|
|
124
|
+
return "\n".join(reversed(out))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def mirror_to_auto_log(name: str, kind: str, result: dict | None,
|
|
128
|
+
duration_s: float = 0.0, *, detail: str = "",
|
|
129
|
+
task_id: str = "") -> bool:
|
|
130
|
+
"""Append a one-line summary of an agent run to ``~/.agentos/auto.log``.
|
|
131
|
+
|
|
132
|
+
`agentos-auto logs [-f]` tails that file and nothing else, so before this an
|
|
133
|
+
agent run — including a ticket-triggered one — was invisible there: it landed
|
|
134
|
+
in `agent_outputs/<agent>/*.md` and `usage.jsonl`, and an operator watching
|
|
135
|
+
the log saw the ticket loop go quiet with no explanation. Mirroring here
|
|
136
|
+
rather than at each trigger site is deliberate: every VPS-side run already
|
|
137
|
+
funnels through `record_run`, so one append covers the panel, the CLI, an
|
|
138
|
+
agentd cron schedule, an agentd ticket-label trigger and the IPC bridge.
|
|
139
|
+
|
|
140
|
+
A summary, not a transcript — the transcript stays in `agent_outputs/`, and
|
|
141
|
+
the pointer below is how you get from one to the other. Best-effort and
|
|
142
|
+
silent on failure: a log line must never break a run. Set
|
|
143
|
+
``AGENTOS_NO_AGENT_LOG_MIRROR=1`` to turn it off.
|
|
144
|
+
"""
|
|
145
|
+
if os.getenv("AGENTOS_NO_AGENT_LOG_MIRROR"):
|
|
146
|
+
return False
|
|
147
|
+
try:
|
|
148
|
+
from sdk.agents.run_log import agent_dir
|
|
149
|
+
from sdk.run_auto import _LOG_FILE
|
|
150
|
+
except Exception: # noqa: BLE001 — never break a run over a log line
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
res = result or {}
|
|
154
|
+
stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
|
|
155
|
+
line = f" • [{stamp}] {describe(name, kind, res, duration_s, detail)}"
|
|
156
|
+
# agentd passes the ticket BOTH as `detail` ("task T1") and as `task_id`, so an
|
|
157
|
+
# unconditional suffix renders "… — task T1 · task T1".
|
|
158
|
+
if task_id and f"task {task_id}" not in line:
|
|
159
|
+
line += f" · task {task_id}"
|
|
160
|
+
try:
|
|
161
|
+
where = agent_dir(name)
|
|
162
|
+
except Exception: # noqa: BLE001
|
|
163
|
+
where = None
|
|
164
|
+
if where:
|
|
165
|
+
line += f"\n ↳ transcript: {where}"
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
with _LOG_FILE.open("a", encoding="utf-8") as fh:
|
|
170
|
+
fh.write(line + "\n")
|
|
171
|
+
return True
|
|
172
|
+
except Exception as exc: # noqa: BLE001
|
|
173
|
+
logger.debug("auto.log mirror failed agent=%s err=%s", name, exc)
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
178
|
+
*, client: "BackendClient | None" = None, detail: str = "",
|
|
179
|
+
task_id: str = "", lines: list[str] | None = None) -> bool:
|
|
180
|
+
"""Emit ONE ``{activity, agentId[, taskId]}`` event for a finished agent run.
|
|
181
|
+
|
|
182
|
+
Returns True when the webhook accepted it, False otherwise (unknown agent,
|
|
183
|
+
no token, backend down) — never raises.
|
|
184
|
+
|
|
185
|
+
``task_id`` attributes a ticket-triggered run to its ticket. A scheduled or
|
|
186
|
+
CLI run has no ticket and omits it.
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
if client is None:
|
|
190
|
+
from sdk.client import BackendClient
|
|
191
|
+
client = BackendClient.from_config()
|
|
192
|
+
agent_id = await client.resolve_agent_id(name)
|
|
193
|
+
if not agent_id:
|
|
194
|
+
# Local-only agent → not tracked. This used to return in silence,
|
|
195
|
+
# which is why "the webhook never fires" had no diagnosable cause:
|
|
196
|
+
# the run succeeded, the ledger recorded it, and the only missing
|
|
197
|
+
# thing was a Postgres row nobody could tie back to a decision.
|
|
198
|
+
# `resolve_agent_id` logs WHY (unreachable listing vs genuinely
|
|
199
|
+
# absent); this says what it cost.
|
|
200
|
+
logger.warning(
|
|
201
|
+
"agent %r has no backend agentId — skipping the agent-activity "
|
|
202
|
+
"webhook, so this run will not appear in agent_activities. It is "
|
|
203
|
+
"still in the local ledger and in agent_outputs/.", name)
|
|
204
|
+
return False
|
|
205
|
+
activity_text = describe(name, kind, result, duration_s, detail)
|
|
206
|
+
block = _lines_block(lines or [])
|
|
207
|
+
if block:
|
|
208
|
+
activity_text += "\n" + block
|
|
209
|
+
return await client.log_agent_activity(activity_text, agent_id, task_id=task_id)
|
|
210
|
+
except Exception as exc: # noqa: BLE001 — activity emit must never break a run
|
|
211
|
+
logger.warning("activity emit failed agent=%s err=%s", name, exc)
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
216
|
+
*, client: "BackendClient | None" = None, detail: str = "",
|
|
217
|
+
task_id: str = "") -> None:
|
|
218
|
+
"""Record a finished run to BOTH sinks: local ledger, then platform feed.
|
|
219
|
+
|
|
220
|
+
The ledger write happens first and independently of the webhook, so a
|
|
221
|
+
backend outage still leaves per-agent utilisation on the VPS (which is what
|
|
222
|
+
``/api/ui/agents/usage`` and the report's "VPS Agent Logs" read).
|
|
223
|
+
"""
|
|
224
|
+
res: dict[str, Any] = result or {}
|
|
225
|
+
try:
|
|
226
|
+
from sdk.agents import usage as usage_mod
|
|
227
|
+
usage_mod.record(
|
|
228
|
+
name,
|
|
229
|
+
kind=kind,
|
|
230
|
+
duration_s=duration_s,
|
|
231
|
+
input_tokens=res.get("input_tokens", 0) or 0,
|
|
232
|
+
output_tokens=res.get("output_tokens", 0) or 0,
|
|
233
|
+
cost_usd=res.get("cost_usd", 0.0) or 0.0,
|
|
234
|
+
ok=bool(res.get("ok")),
|
|
235
|
+
)
|
|
236
|
+
except Exception as exc: # noqa: BLE001 — ledger write must never break a run
|
|
237
|
+
logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
|
|
238
|
+
|
|
239
|
+
mirror_to_auto_log(name, kind, res, duration_s, detail=detail, task_id=task_id)
|
|
240
|
+
|
|
241
|
+
# Whatever this run's log lines recorded travels with the ONE event, so the
|
|
242
|
+
# activity feed shows what the agent did, not just that it finished.
|
|
243
|
+
try:
|
|
244
|
+
from sdk.agents.run_log import _folder_name
|
|
245
|
+
lines = drain(_folder_name(name))
|
|
246
|
+
except Exception: # noqa: BLE001
|
|
247
|
+
lines = []
|
|
248
|
+
|
|
249
|
+
await emit(name, kind, res, duration_s, client=client, detail=detail,
|
|
250
|
+
task_id=task_id, lines=lines)
|
|
@@ -175,7 +175,13 @@ def start_run(agent_name: str, *, scope: str = "custom", engine: str = "",
|
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
def log_line(path: Path | None, text: str) -> None:
|
|
178
|
-
"""Append a raw line to the activity block (best-effort, no-op if path is None).
|
|
178
|
+
"""Append a raw line to the activity block (best-effort, no-op if path is None).
|
|
179
|
+
|
|
180
|
+
The line is also noted for the run's activity event (``activity.note``), so
|
|
181
|
+
what the run did reaches the platform feed and not merely the fact that it
|
|
182
|
+
finished. That is a buffer append with no I/O — a POST per line is what
|
|
183
|
+
saturated the shared Postgres pool and is why the coalescing exists.
|
|
184
|
+
"""
|
|
179
185
|
if not path:
|
|
180
186
|
return
|
|
181
187
|
try:
|
|
@@ -183,6 +189,11 @@ def log_line(path: Path | None, text: str) -> None:
|
|
|
183
189
|
fh.write(f"[{_now()}] {text}\n")
|
|
184
190
|
except Exception as exc: # noqa: BLE001
|
|
185
191
|
logger.debug("run_log.line failed: %s", exc)
|
|
192
|
+
try:
|
|
193
|
+
from sdk.agents import activity
|
|
194
|
+
activity.note(path.parent.name, str(text))
|
|
195
|
+
except Exception as exc: # noqa: BLE001 — a feed note must never break a run
|
|
196
|
+
logger.debug("run_log.note failed: %s", exc)
|
|
186
197
|
|
|
187
198
|
|
|
188
199
|
def log_event(path: Path | None, event: dict) -> None:
|
|
@@ -12,6 +12,14 @@ Command surface:
|
|
|
12
12
|
Store (or replace) the co_<uuid>_<hex> backend token. See
|
|
13
13
|
sdk.config.configure_cli.
|
|
14
14
|
|
|
15
|
+
agentos chat [--product-id ID] [--pv-id ID] [--no-autorun]
|
|
16
|
+
Describe work in plain language; the backend asks the follow-up
|
|
17
|
+
questions until it has a goal, scope, acceptance criteria,
|
|
18
|
+
constraints, owning department and priority, then builds the plan,
|
|
19
|
+
creates the ticket(s) and wakes the auto-loop. The new ticket still
|
|
20
|
+
runs in createdAt order behind anything already queued.
|
|
21
|
+
See sdk.intake.chat_cli.
|
|
22
|
+
|
|
15
23
|
agentos agent new
|
|
16
24
|
Interactive wizard (input() prompts) — name, description, engine
|
|
17
25
|
(default "claude_code"), trigger type (schedule/ticket_label/manual/
|
|
@@ -86,6 +94,15 @@ def main() -> None:
|
|
|
86
94
|
configure_cli(rotate="--rotate" in sys.argv)
|
|
87
95
|
return
|
|
88
96
|
|
|
97
|
+
if noun == "chat":
|
|
98
|
+
from sdk.intake import IntakeError, chat_cli
|
|
99
|
+
try:
|
|
100
|
+
chat_cli(rest)
|
|
101
|
+
except IntakeError as exc:
|
|
102
|
+
print(f"error: {exc}")
|
|
103
|
+
sys.exit(1)
|
|
104
|
+
return
|
|
105
|
+
|
|
89
106
|
if noun == "agent":
|
|
90
107
|
_agent_command(rest)
|
|
91
108
|
return
|
|
@@ -111,9 +128,10 @@ def main() -> None:
|
|
|
111
128
|
|
|
112
129
|
|
|
113
130
|
def _print_usage() -> None:
|
|
114
|
-
print("usage: agentos <configure|agent|integration|default-agents|agentd|github> ...")
|
|
131
|
+
print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github> ...")
|
|
115
132
|
print()
|
|
116
133
|
print(" agentos configure [--rotate]")
|
|
134
|
+
print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
|
|
117
135
|
print(" agentos agent <list|run NAME|chat NAME MSG|logs NAME|enable NAME|disable NAME|rm NAME>")
|
|
118
136
|
print(" agentos agent <set-key NAME KEY[=VALUE]…|keys NAME|rm-key NAME KEY…> # persistent API keys (custom agents)")
|
|
119
137
|
print(" agentos integration <add PATH|test NAME|list|rm NAME>")
|
|
@@ -45,6 +45,11 @@ 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
|
+
_GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
|
|
51
|
+
_GITHUB_TOKEN_TTL_S = float(os.getenv("AGENTOS_GITHUB_TOKEN_TTL_S", "") or 1800.0)
|
|
52
|
+
|
|
48
53
|
# ── Ticket status-change observers ────────────────────────────────────────
|
|
49
54
|
# Best-effort, synchronous hooks fired when the client changes a ticket's backend
|
|
50
55
|
# status (In Progress / Backlog / To Do). The CLI loop registers one to track
|
|
@@ -928,9 +933,25 @@ class BackendClient:
|
|
|
928
933
|
endpoint lives on the ML backend (devopsbot-be), authorised by the same
|
|
929
934
|
co_ token. Best-effort: returns None on any failure so a run without a
|
|
930
935
|
connected GitHub account still proceeds (clone/PR steps just won't auth).
|
|
936
|
+
|
|
937
|
+
Memoized for _GITHUB_TOKEN_TTL_S. The auto-loop asks for this once per
|
|
938
|
+
ticket and again inside epic/story handling, so a long queue made the same
|
|
939
|
+
round-trip dozens of times for a value that does not change between
|
|
940
|
+
tickets. A TTL rather than a permanent cache so a token refreshed on the
|
|
941
|
+
backend is picked up without restarting the daemon; a failed fetch is not
|
|
942
|
+
cached, so a transient outage cannot pin None for the whole window.
|
|
931
943
|
"""
|
|
944
|
+
import time as _time
|
|
945
|
+
|
|
946
|
+
now = _time.monotonic()
|
|
947
|
+
cached = _GITHUB_TOKEN_CACHE.get(self._token)
|
|
948
|
+
if cached and now < cached[1]:
|
|
949
|
+
return cached[0]
|
|
932
950
|
record = await self.github_token_record()
|
|
933
|
-
|
|
951
|
+
token = (record or {}).get("accessToken") or None
|
|
952
|
+
if token:
|
|
953
|
+
_GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
|
|
954
|
+
return token
|
|
934
955
|
|
|
935
956
|
async def ingest_github_prs(
|
|
936
957
|
self,
|
|
@@ -1435,23 +1456,79 @@ class BackendClient:
|
|
|
1435
1456
|
Returns None when the name isn't a backend agent (e.g. a local-only manifest)
|
|
1436
1457
|
— the caller then skips activity emit, so only backend agents are tracked.
|
|
1437
1458
|
Cached per process (the id↔name mapping is stable within a session).
|
|
1459
|
+
|
|
1460
|
+
Two things this deliberately does NOT do:
|
|
1461
|
+
|
|
1462
|
+
* **Negative-cache a lookup that failed.** Both listings are best-effort, so
|
|
1463
|
+
a transient 5xx or timeout used to leave `rows` empty, be recorded as
|
|
1464
|
+
"not a backend agent", and disable activity reporting for that agent for
|
|
1465
|
+
the rest of the process — a daemon restart was the only cure, and nothing
|
|
1466
|
+
said why. A failed lookup now returns None WITHOUT caching, so the next
|
|
1467
|
+
run retries.
|
|
1468
|
+
* **Insist on byte-identical names.** The name comes from a local manifest
|
|
1469
|
+
or a caller argument while the row is authored in the web app, so casing
|
|
1470
|
+
and stray whitespace drift. Exact match wins; a casefolded match is
|
|
1471
|
+
accepted as a fallback and logged, because a silent near-miss here
|
|
1472
|
+
presents as "the webhook never fires" with no other symptom.
|
|
1438
1473
|
"""
|
|
1439
1474
|
nm = (name or "").strip()
|
|
1440
1475
|
if not nm:
|
|
1441
1476
|
return None
|
|
1442
1477
|
if nm in _AGENT_ID_CACHE:
|
|
1443
1478
|
return _AGENT_ID_CACHE[nm] or None
|
|
1479
|
+
|
|
1480
|
+
# The ML gateway first: it owns the `agents` table that
|
|
1481
|
+
# `agent_activities.agent_id` points at, and answers both scopes in one
|
|
1482
|
+
# call. The AI backend's two per-scope endpoints are the fallback.
|
|
1444
1483
|
rows: list[dict] = []
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1484
|
+
failures = 0
|
|
1485
|
+
try:
|
|
1486
|
+
rows = await self.list_gateway_agents()
|
|
1487
|
+
except Exception as exc: # noqa: BLE001
|
|
1488
|
+
logger.warning("client.resolve_agent_id gateway listing failed err=%s", exc)
|
|
1489
|
+
if not rows:
|
|
1490
|
+
for fetch in (self.list_custom_agents, self.list_department_agents):
|
|
1491
|
+
try:
|
|
1492
|
+
rows += await fetch()
|
|
1493
|
+
except Exception as exc: # noqa: BLE001 — best-effort across sources
|
|
1494
|
+
failures += 1
|
|
1495
|
+
logger.warning("client.resolve_agent_id listing failed source=%s err=%s",
|
|
1496
|
+
getattr(fetch, "__name__", "?"), exc)
|
|
1497
|
+
if not rows and not failures:
|
|
1498
|
+
# Both fallbacks answered, both empty: nothing is visible to this
|
|
1499
|
+
# token at all, which is not the same as "this name is absent".
|
|
1500
|
+
failures = 1
|
|
1501
|
+
|
|
1450
1502
|
for r in rows:
|
|
1451
1503
|
if r.get("name") == nm and r.get("id"):
|
|
1452
1504
|
_AGENT_ID_CACHE[nm] = str(r["id"])
|
|
1453
1505
|
return _AGENT_ID_CACHE[nm]
|
|
1454
|
-
|
|
1506
|
+
|
|
1507
|
+
folded = nm.casefold()
|
|
1508
|
+
for r in rows:
|
|
1509
|
+
if str(r.get("name") or "").strip().casefold() == folded and r.get("id"):
|
|
1510
|
+
logger.warning(
|
|
1511
|
+
"client.resolve_agent_id matched %r to backend agent %r by "
|
|
1512
|
+
"casefolded name — align the names to make this exact",
|
|
1513
|
+
nm, r.get("name"),
|
|
1514
|
+
)
|
|
1515
|
+
_AGENT_ID_CACHE[nm] = str(r["id"])
|
|
1516
|
+
return _AGENT_ID_CACHE[nm]
|
|
1517
|
+
|
|
1518
|
+
if failures and not rows:
|
|
1519
|
+
# Could not see the catalogue at all — "absent" is not established.
|
|
1520
|
+
logger.warning(
|
|
1521
|
+
"client.resolve_agent_id could not reach any agent listing; not "
|
|
1522
|
+
"caching a negative for %r (activity will retry next run)", nm)
|
|
1523
|
+
return None
|
|
1524
|
+
|
|
1525
|
+
logger.warning(
|
|
1526
|
+
"client.resolve_agent_id: %r is not a backend agent for this token "
|
|
1527
|
+
"(%d visible: %s) — activity will NOT be recorded in agent_activities",
|
|
1528
|
+
nm, len(rows),
|
|
1529
|
+
", ".join(sorted(str(r.get("name")) for r in rows if r.get("name")))[:300],
|
|
1530
|
+
)
|
|
1531
|
+
_AGENT_ID_CACHE[nm] = "" # negative-cache: genuinely not a backend agent
|
|
1455
1532
|
return None
|
|
1456
1533
|
|
|
1457
1534
|
async def report_vps_allocation(self, payload: dict) -> bool:
|
|
@@ -1687,6 +1764,44 @@ class BackendClient:
|
|
|
1687
1764
|
_check_status(resp.status_code, self._base_url)
|
|
1688
1765
|
return resp.json().get("agents", [])
|
|
1689
1766
|
|
|
1767
|
+
async def list_gateway_agents(self) -> list[dict]:
|
|
1768
|
+
"""GET ``{ML_GATEWAY_URL}/ml-api/agents`` — every agent this token can see.
|
|
1769
|
+
|
|
1770
|
+
One call returns BOTH scopes: ``data.personal`` (the caller's own custom
|
|
1771
|
+
agents) and ``data.department`` (their department's defaults). Rows carry
|
|
1772
|
+
the same ``id``/``name`` shape as the AI backend's two per-scope endpoints.
|
|
1773
|
+
|
|
1774
|
+
This is the authoritative list for activity reporting, and the reason it is
|
|
1775
|
+
preferred over ``list_custom_agents`` + ``list_department_agents``:
|
|
1776
|
+
``agent_activities.agent_id`` is a foreign key onto ``agents.id`` in THIS
|
|
1777
|
+
service's Postgres, and this endpoint reads that same table. The AI
|
|
1778
|
+
backend's listings are a re-projection of it — equivalent today (verified:
|
|
1779
|
+
identical ids for all six agents) but able to drift, and they cost two
|
|
1780
|
+
round-trips plus a dependency on a second service being up.
|
|
1781
|
+
|
|
1782
|
+
Best-effort: returns [] rather than raising, so a caller can fall back.
|
|
1783
|
+
"""
|
|
1784
|
+
from sdk.config import ML_GATEWAY_URL
|
|
1785
|
+
|
|
1786
|
+
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agents"
|
|
1787
|
+
headers = {**self._headers, "accept": "*/*",
|
|
1788
|
+
"developer-name": "null", "product": ""}
|
|
1789
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
1790
|
+
resp = await client.get(url, headers=headers)
|
|
1791
|
+
if resp.status_code >= 400:
|
|
1792
|
+
logger.warning("client.list_gateway_agents failed status=%s body=%s",
|
|
1793
|
+
resp.status_code, resp.text[:200])
|
|
1794
|
+
return []
|
|
1795
|
+
data = (resp.json() or {}).get("data") or {}
|
|
1796
|
+
if not isinstance(data, dict):
|
|
1797
|
+
return []
|
|
1798
|
+
rows: list[dict] = []
|
|
1799
|
+
for scope in ("personal", "department"):
|
|
1800
|
+
got = data.get(scope)
|
|
1801
|
+
if isinstance(got, list):
|
|
1802
|
+
rows += [r for r in got if isinstance(r, dict)]
|
|
1803
|
+
return rows
|
|
1804
|
+
|
|
1690
1805
|
async def list_product_agents(self, product_id: str) -> dict:
|
|
1691
1806
|
"""GET /api/v1/product/agents?product_id=… — a product's custom agents.
|
|
1692
1807
|
|
|
@@ -151,6 +151,17 @@ def save(base_url: str, token: str) -> None:
|
|
|
151
151
|
|
|
152
152
|
|
|
153
153
|
def get_base_url() -> str:
|
|
154
|
+
"""The AI backend this machine talks to.
|
|
155
|
+
|
|
156
|
+
``AGENTOS_AI_URL`` overrides the stored value, so a local backend can be
|
|
157
|
+
pointed at for a test run without rewriting config.toml — which is the state
|
|
158
|
+
you do not want to leave a real box in. Same precedence as the other
|
|
159
|
+
overrides here: env, then ``~/.agentos/.env``, then stored, then the default.
|
|
160
|
+
"""
|
|
161
|
+
override = (os.environ.get("AGENTOS_AI_URL", "")
|
|
162
|
+
or _dotenv_value("AGENTOS_AI_URL")).strip()
|
|
163
|
+
if override:
|
|
164
|
+
return override.rstrip("/")
|
|
154
165
|
return load().get("base_url", AI_DEFAULT_URL)
|
|
155
166
|
|
|
156
167
|
|