devops-bot-sdk 1.6.14__tar.gz → 1.6.19__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.14 → devops_bot_sdk-1.6.19}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/SOURCES.txt +14 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/__init__.py +16 -2
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agentd.py +12 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/activity.py +11 -4
- devops_bot_sdk-1.6.19/sdk/agents/ticket_run.py +325 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/client.py +327 -34
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/config.py +111 -9
- devops_bot_sdk-1.6.19/sdk/departments.py +206 -0
- devops_bot_sdk-1.6.19/sdk/formats.py +82 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/github_ingest.py +196 -34
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/handlers.py +492 -91
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/local_exec.py +23 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/run_auto.py +225 -10
- devops_bot_sdk-1.6.19/sdk/ticket_fields.py +149 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/service.py +62 -5
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_activity.py +3 -1
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_run_ticket.py +49 -0
- devops_bot_sdk-1.6.19/tests/test_auto_agent_dispatch.py +812 -0
- devops_bot_sdk-1.6.19/tests/test_client_jira_projects.py +246 -0
- devops_bot_sdk-1.6.19/tests/test_config_token_diagnostics.py +88 -0
- devops_bot_sdk-1.6.19/tests/test_configure_token_owner.py +59 -0
- devops_bot_sdk-1.6.19/tests/test_departments.py +514 -0
- devops_bot_sdk-1.6.19/tests/test_epic_inherited_agent_id.py +216 -0
- devops_bot_sdk-1.6.19/tests/test_formats.py +209 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_github_ingest.py +99 -11
- devops_bot_sdk-1.6.19/tests/test_run_auto_agent_only.py +577 -0
- devops_bot_sdk-1.6.19/tests/test_service_token_warning.py +88 -0
- devops_bot_sdk-1.6.19/tests/test_ticket_fields.py +402 -0
- devops_bot_sdk-1.6.19/tests/test_ticket_run_resolve_by_id.py +508 -0
- devops_bot_sdk-1.6.14/sdk/agents/ticket_run.py +0 -129
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/README.md +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.14 → devops_bot_sdk-1.6.19}/tests/test_vps_health.py +0 -0
|
@@ -12,7 +12,9 @@ sdk/cli.py
|
|
|
12
12
|
sdk/client.py
|
|
13
13
|
sdk/config.py
|
|
14
14
|
sdk/crucial.py
|
|
15
|
+
sdk/departments.py
|
|
15
16
|
sdk/exceptions.py
|
|
17
|
+
sdk/formats.py
|
|
16
18
|
sdk/git_ops.py
|
|
17
19
|
sdk/github_ingest.py
|
|
18
20
|
sdk/graphify.py
|
|
@@ -24,6 +26,7 @@ sdk/scheduler.py
|
|
|
24
26
|
sdk/sse.py
|
|
25
27
|
sdk/test.py
|
|
26
28
|
sdk/test_pipeline.py
|
|
29
|
+
sdk/ticket_fields.py
|
|
27
30
|
sdk/updater.py
|
|
28
31
|
sdk/vps_health.py
|
|
29
32
|
sdk/agents/__init__.py
|
|
@@ -104,14 +107,25 @@ tests/test_agent_schedules.py
|
|
|
104
107
|
tests/test_agent_workspace_settings.py
|
|
105
108
|
tests/test_agents_connectors_builder.py
|
|
106
109
|
tests/test_allowed_tools_config.py
|
|
110
|
+
tests/test_auto_agent_dispatch.py
|
|
111
|
+
tests/test_client_jira_projects.py
|
|
112
|
+
tests/test_config_token_diagnostics.py
|
|
113
|
+
tests/test_configure_token_owner.py
|
|
107
114
|
tests/test_console_encoding.py
|
|
115
|
+
tests/test_departments.py
|
|
116
|
+
tests/test_epic_inherited_agent_id.py
|
|
117
|
+
tests/test_formats.py
|
|
108
118
|
tests/test_git_action.py
|
|
109
119
|
tests/test_github_ingest.py
|
|
110
120
|
tests/test_health.py
|
|
111
121
|
tests/test_otp_login.py
|
|
122
|
+
tests/test_run_auto_agent_only.py
|
|
112
123
|
tests/test_run_outputs.py
|
|
113
124
|
tests/test_schedule_api.py
|
|
114
125
|
tests/test_scheduler.py
|
|
126
|
+
tests/test_service_token_warning.py
|
|
127
|
+
tests/test_ticket_fields.py
|
|
128
|
+
tests/test_ticket_run_resolve_by_id.py
|
|
115
129
|
tests/test_update_timer.py
|
|
116
130
|
tests/test_updater_extras.py
|
|
117
131
|
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.19
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -19,6 +19,10 @@ Collectors (payload producers — submit via BackendClient.submit_webhook):
|
|
|
19
19
|
collectors.screenshot.build_webhook_payload(token, project)
|
|
20
20
|
collectors.process.build_webhook_payload(token, project)
|
|
21
21
|
|
|
22
|
+
Standard artifact formats (house style for generated Epics/Stories/PRDs):
|
|
23
|
+
formats.EPIC_FORMAT, formats.USER_STORY_FORMAT, formats.PRD_FORMAT
|
|
24
|
+
formats.FORMAT_VERSION — generation stamp for the three above
|
|
25
|
+
|
|
22
26
|
IPC entry point (Electron main process):
|
|
23
27
|
python -m sdk.ipc.electron_bridge (stdio newline-delimited JSON)
|
|
24
28
|
|
|
@@ -30,7 +34,7 @@ Rules:
|
|
|
30
34
|
- All data egress through submit_webhook only
|
|
31
35
|
"""
|
|
32
36
|
|
|
33
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.19" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
34
38
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
35
39
|
|
|
36
40
|
from sdk.client import BackendClient
|
|
@@ -41,6 +45,12 @@ from sdk.exceptions import (
|
|
|
41
45
|
CollectorError,
|
|
42
46
|
TokenNotConfigured,
|
|
43
47
|
)
|
|
48
|
+
from sdk.formats import (
|
|
49
|
+
EPIC_FORMAT,
|
|
50
|
+
FORMAT_VERSION,
|
|
51
|
+
PRD_FORMAT,
|
|
52
|
+
USER_STORY_FORMAT,
|
|
53
|
+
)
|
|
44
54
|
from sdk.models.envelope import Envelope
|
|
45
55
|
|
|
46
56
|
__all__ = [
|
|
@@ -52,4 +62,8 @@ __all__ = [
|
|
|
52
62
|
"BackendVersionTooOld",
|
|
53
63
|
"CollectorError",
|
|
54
64
|
"TokenNotConfigured",
|
|
65
|
+
"EPIC_FORMAT",
|
|
66
|
+
"USER_STORY_FORMAT",
|
|
67
|
+
"PRD_FORMAT",
|
|
68
|
+
"FORMAT_VERSION",
|
|
55
69
|
]
|
|
@@ -346,10 +346,20 @@ async def _dispatch_agent_run(
|
|
|
346
346
|
if skill_text:
|
|
347
347
|
prompt_text = f"{prompt_text}\n\n{skill_text}"
|
|
348
348
|
|
|
349
|
+
# When a TICKET triggered this run, the backend must drive ITS ML_TASK_FLOW
|
|
350
|
+
# lifecycle — that flow is a no-op unless `jira_task_id` is set (see
|
|
351
|
+
# sdk/models/requests.py). Without it nothing ever moves the ticket out of
|
|
352
|
+
# "To Do", so the next auto pass re-fetches and re-executes the same agent
|
|
353
|
+
# forever. A scheduled (task=None) run has no ticket and stays session-only,
|
|
354
|
+
# exactly as before. It also makes `_on_status_update` below address the
|
|
355
|
+
# TICKET id rather than the session id on the approval_reply round-trip.
|
|
356
|
+
jira_task_id = str((task or {}).get("id") or "") or None
|
|
357
|
+
|
|
349
358
|
req = OrchestrateRequest(
|
|
350
359
|
user_input=prompt_text,
|
|
351
360
|
local_execution=True,
|
|
352
361
|
dev_engine="claude_code",
|
|
362
|
+
jira_task_id=jira_task_id,
|
|
353
363
|
metadata={"custom_agent_id": manifest.name, "custom_agent_name": manifest.name},
|
|
354
364
|
)
|
|
355
365
|
|
|
@@ -420,6 +430,7 @@ async def _dispatch_agent_run(
|
|
|
420
430
|
manifest.name, _activity_kind, local_result,
|
|
421
431
|
time.monotonic() - _started, client=client,
|
|
422
432
|
detail=f"task {task_id}" if task_id else "",
|
|
433
|
+
task_id=task_id or "",
|
|
423
434
|
)
|
|
424
435
|
outcome.update({
|
|
425
436
|
"task_id": task_id,
|
|
@@ -435,6 +446,7 @@ async def _dispatch_agent_run(
|
|
|
435
446
|
await activity.record_run(
|
|
436
447
|
manifest.name, _activity_kind, {"ok": False, "error": str(exc)},
|
|
437
448
|
0.0, client=client, detail=f"task {task_id}" if task_id else "",
|
|
449
|
+
task_id=task_id or "",
|
|
438
450
|
)
|
|
439
451
|
return
|
|
440
452
|
|
|
@@ -63,11 +63,15 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
66
|
-
*, client: "BackendClient | None" = None, detail: str = ""
|
|
67
|
-
|
|
66
|
+
*, client: "BackendClient | None" = None, detail: str = "",
|
|
67
|
+
task_id: str = "") -> bool:
|
|
68
|
+
"""Emit ONE ``{activity, agentId[, taskId]}`` event for a finished agent run.
|
|
68
69
|
|
|
69
70
|
Returns True when the webhook accepted it, False otherwise (unknown agent,
|
|
70
71
|
no token, backend down) — never raises.
|
|
72
|
+
|
|
73
|
+
``task_id`` attributes a ticket-triggered run to its ticket. A scheduled or
|
|
74
|
+
CLI run has no ticket and omits it.
|
|
71
75
|
"""
|
|
72
76
|
try:
|
|
73
77
|
if client is None:
|
|
@@ -78,6 +82,7 @@ async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.
|
|
|
78
82
|
return False
|
|
79
83
|
return await client.log_agent_activity(
|
|
80
84
|
describe(name, kind, result, duration_s, detail), agent_id,
|
|
85
|
+
task_id=task_id,
|
|
81
86
|
)
|
|
82
87
|
except Exception as exc: # noqa: BLE001 — activity emit must never break a run
|
|
83
88
|
logger.debug("activity emit failed agent=%s err=%s", name, exc)
|
|
@@ -85,7 +90,8 @@ async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.
|
|
|
85
90
|
|
|
86
91
|
|
|
87
92
|
async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
88
|
-
*, client: "BackendClient | None" = None, detail: str = ""
|
|
93
|
+
*, client: "BackendClient | None" = None, detail: str = "",
|
|
94
|
+
task_id: str = "") -> None:
|
|
89
95
|
"""Record a finished run to BOTH sinks: local ledger, then platform feed.
|
|
90
96
|
|
|
91
97
|
The ledger write happens first and independently of the webhook, so a
|
|
@@ -107,4 +113,5 @@ async def record_run(name: str, kind: str, result: dict | None, duration_s: floa
|
|
|
107
113
|
except Exception as exc: # noqa: BLE001 — ledger write must never break a run
|
|
108
114
|
logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
|
|
109
115
|
|
|
110
|
-
await emit(name, kind, res, duration_s, client=client, detail=detail
|
|
116
|
+
await emit(name, kind, res, duration_s, client=client, detail=detail,
|
|
117
|
+
task_id=task_id)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""On-demand "run an agent ON a Jira ticket" — the manual counterpart to the
|
|
2
|
+
``agentd`` ``ticket_label`` trigger.
|
|
3
|
+
|
|
4
|
+
The daemon fires an agent when a To-Do ticket carries its label; this module
|
|
5
|
+
lets a human pick an agent AND a ticket and run it now (from the UI, the API, or
|
|
6
|
+
the CLI). It reuses the exact same engine — ``agentd._dispatch_agent_run`` — so
|
|
7
|
+
the ticket's fields fill the agent's ``{{variables}}`` and the work is planned,
|
|
8
|
+
gated, executed locally, and reported back to the platform against that ticket.
|
|
9
|
+
|
|
10
|
+
Because a human initiated the run, ``auto_approve=True`` is passed: the GO-AHEAD
|
|
11
|
+
is sent immediately, skipping the agent's ``approval.policy`` WhatsApp gate for
|
|
12
|
+
this single run.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from sdk.agents.manifest import AgentManifest
|
|
20
|
+
from sdk.client import BackendClient
|
|
21
|
+
|
|
22
|
+
# Process-lifetime cache of backend agentId → (row, source) for the agents this
|
|
23
|
+
# caller may run — the INVERSE of ``_AGENT_ID_CACHE`` in sdk/client.py (name→id),
|
|
24
|
+
# and cached for the same reason: one run pass touches many tickets that share an
|
|
25
|
+
# agentId, and each would otherwise cost a pair of list GETs.
|
|
26
|
+
#
|
|
27
|
+
# ``("", …)`` — an empty ``source`` — is the NEGATIVE entry: "no agent with this id
|
|
28
|
+
# is visible to me". Negative caching is what keeps thirty tickets pointing at a
|
|
29
|
+
# revoked agent from re-fetching both lists thirty times. It is only ever written
|
|
30
|
+
# once absence has actually been ESTABLISHED (both lists read without error) — see
|
|
31
|
+
# ``_load_agent_index``'s ``complete`` flag.
|
|
32
|
+
_AGENT_BY_ID_CACHE: dict[str, tuple[dict[str, Any], str]] = {}
|
|
33
|
+
|
|
34
|
+
# The two caller-scoped sources, in precedence order, with the provenance label
|
|
35
|
+
# each one stamps. These labels are the same strings ``sdk/agents/inventory.py``
|
|
36
|
+
# and ``sdk/ui/api.py`` already use, so a caller can compare them directly.
|
|
37
|
+
#
|
|
38
|
+
# ``list_department_agents`` covers department defaults AND org-wide common agents
|
|
39
|
+
# in ONE response (the backend's ``department.list_for_department`` ORs both
|
|
40
|
+
# filters), which is why there is no third source here.
|
|
41
|
+
_ID_SOURCES: tuple[tuple[str, str, str], ...] = (
|
|
42
|
+
("your agents", "list_custom_agents", "custom"),
|
|
43
|
+
("department defaults", "list_department_agents", "default"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ResolvedAgent(NamedTuple):
|
|
48
|
+
"""A backend agent this caller is entitled to run, and where it came from.
|
|
49
|
+
|
|
50
|
+
A ``NamedTuple`` rather than a field on ``AgentManifest``: the manifest is a
|
|
51
|
+
closed model shared with local YAML agents, which have no backend provenance
|
|
52
|
+
to report. ``source`` is ``"custom"`` or ``"default"``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
agent_id: str
|
|
56
|
+
manifest: AgentManifest
|
|
57
|
+
source: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_deleted(row: dict[str, Any]) -> bool:
|
|
61
|
+
"""True when the row is soft-deleted, in any of the spellings a row may use.
|
|
62
|
+
|
|
63
|
+
Defensive: the list endpoints are expected to exclude soft-deleted rows
|
|
64
|
+
server-side, but a manifest resolved here is dispatched with the agent's
|
|
65
|
+
secrets injected as engine env, so the gate is re-asserted locally rather than
|
|
66
|
+
trusted. Fails closed — an unrecognised truthy value counts as deleted.
|
|
67
|
+
"""
|
|
68
|
+
return any(row.get(key) for key in ("deleted_at", "deletedAt", "is_deleted", "isDeleted"))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _load_agent_index(
|
|
72
|
+
client: BackendClient, reasons: list[str] | None = None,
|
|
73
|
+
) -> tuple[bool, int]:
|
|
74
|
+
"""Warm ``_AGENT_BY_ID_CACHE`` from both caller-scoped lists.
|
|
75
|
+
|
|
76
|
+
Returns ``(complete, visible)`` — whether BOTH sources were read without
|
|
77
|
+
error, and how many rows were indexed. ``complete`` is what lets the caller
|
|
78
|
+
tell "this agent is not mine" from "I could not find out": only the former may
|
|
79
|
+
be negative-cached for the process lifetime.
|
|
80
|
+
|
|
81
|
+
Per-source failures are tolerated and recorded, never raised: a
|
|
82
|
+
department-list outage must not hide a custom hit, and vice versa — the same
|
|
83
|
+
posture ``chat.resolve_agent`` takes. A client missing one of the methods
|
|
84
|
+
entirely (an older client, or a test fake) is skipped, and also counts as
|
|
85
|
+
incomplete.
|
|
86
|
+
|
|
87
|
+
``_ID_SOURCES`` is walked in precedence order and a row already written in
|
|
88
|
+
THIS pass is not overwritten, so a custom agent wins an id collision with a
|
|
89
|
+
department one. A row written by an EARLIER pass is refreshed, which is also
|
|
90
|
+
how a negative entry is upgraded once the agent becomes visible.
|
|
91
|
+
"""
|
|
92
|
+
def _note(reason: str) -> None:
|
|
93
|
+
if reasons is not None:
|
|
94
|
+
reasons.append(reason)
|
|
95
|
+
|
|
96
|
+
complete = True
|
|
97
|
+
seen: set[str] = set()
|
|
98
|
+
|
|
99
|
+
for label, attr, source in _ID_SOURCES:
|
|
100
|
+
fetch = getattr(client, attr, None)
|
|
101
|
+
if fetch is None:
|
|
102
|
+
complete = False
|
|
103
|
+
continue
|
|
104
|
+
try:
|
|
105
|
+
rows = await fetch() or []
|
|
106
|
+
except Exception as exc: # noqa: BLE001 — a fetch failure is not "not mine"
|
|
107
|
+
# str() is EMPTY on some httpx errors (ReadTimeout) — name the class then.
|
|
108
|
+
complete = False
|
|
109
|
+
_note(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
|
|
110
|
+
continue
|
|
111
|
+
for row in rows:
|
|
112
|
+
if not isinstance(row, dict):
|
|
113
|
+
continue
|
|
114
|
+
row_id = str(row.get("id") or "").strip()
|
|
115
|
+
if not row_id or row_id in seen:
|
|
116
|
+
continue
|
|
117
|
+
seen.add(row_id)
|
|
118
|
+
_AGENT_BY_ID_CACHE[row_id] = (row, source)
|
|
119
|
+
|
|
120
|
+
return complete, len(seen)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def resolve_manifest_by_id(
|
|
124
|
+
client: BackendClient, agent_id: str, reasons: list[str] | None = None,
|
|
125
|
+
) -> ResolvedAgent | None:
|
|
126
|
+
"""Find the runnable manifest for a ticket's ``agentId``, scoped to the caller.
|
|
127
|
+
|
|
128
|
+
The counterpart to :func:`resolve_manifest`, which resolves by NAME. A ticket
|
|
129
|
+
carries an id, so this walks the id instead — but the important difference is
|
|
130
|
+
not the key, it is the SCOPE.
|
|
131
|
+
|
|
132
|
+
**Scope is the authorisation check.** The only agents reachable here are those
|
|
133
|
+
in the union of ``list_custom_agents()`` (the caller's own) and
|
|
134
|
+
``list_department_agents()`` (their department defaults plus org-wide common
|
|
135
|
+
agents — one response covers both). Membership in that union IS the
|
|
136
|
+
entitlement: no ``relation_id`` comparison, no department-id argument, and
|
|
137
|
+
deliberately no fetch-by-id endpoint. An id that exists platform-side but is
|
|
138
|
+
absent from both responses is simply unresolvable, and nothing further is
|
|
139
|
+
tried. That matters because a resolved manifest carries a system prompt and is
|
|
140
|
+
handed to ``agentd._dispatch_agent_run``, which runs it on THIS machine with
|
|
141
|
+
the agent's resolved secrets injected as engine env — so a broader lookup
|
|
142
|
+
would be a privilege escalation, not a convenience.
|
|
143
|
+
|
|
144
|
+
There is also no local-manifest fallback (``resolve_manifest``'s ``custom``
|
|
145
|
+
scope has one): a hand-written ``~/.agentos/agents`` YAML has no backend id,
|
|
146
|
+
so it is unreachable by id by construction — and honouring a local override
|
|
147
|
+
here would let a local file stand in for an agent the caller may not run.
|
|
148
|
+
|
|
149
|
+
Returns ``None`` — never a manifest — for a blank id, an id in neither list, a
|
|
150
|
+
non-``active`` status, a soft-deleted row, or a row with no usable prompt. Pass
|
|
151
|
+
``reasons``, a list this appends to, to learn WHICH: exactly as with
|
|
152
|
+
``resolve_manifest``, "not yours", "your token is not valid so the list could
|
|
153
|
+
not be read", "it is paused" and "it has no prompt" are four different
|
|
154
|
+
problems that all end in ``None``, and they demand different fixes.
|
|
155
|
+
|
|
156
|
+
Results are memoised for the process lifetime, positive AND negative, so N
|
|
157
|
+
tickets sharing one agentId cost one pair of list GETs. The runnability gate
|
|
158
|
+
below runs on EVERY call rather than at cache-fill time, so a cached-but-paused
|
|
159
|
+
agent answers ``None`` every time — from cache, with no extra fetch.
|
|
160
|
+
"""
|
|
161
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
162
|
+
|
|
163
|
+
def _note(reason: str) -> None:
|
|
164
|
+
if reasons is not None:
|
|
165
|
+
reasons.append(reason)
|
|
166
|
+
|
|
167
|
+
aid = str(agent_id or "").strip()
|
|
168
|
+
if not aid:
|
|
169
|
+
# No network call: a ticket with no agentId is not a lookup failure.
|
|
170
|
+
_note("the ticket carries no agentId, so there is nothing to resolve")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
entry = _AGENT_BY_ID_CACHE.get(aid)
|
|
174
|
+
if entry is None:
|
|
175
|
+
complete, visible = await _load_agent_index(client, reasons)
|
|
176
|
+
entry = _AGENT_BY_ID_CACHE.get(aid)
|
|
177
|
+
if entry is None:
|
|
178
|
+
if complete:
|
|
179
|
+
# Absence is ESTABLISHED — safe to remember.
|
|
180
|
+
_AGENT_BY_ID_CACHE[aid] = ({}, "")
|
|
181
|
+
_note(f"no agent with id '{aid}' is in your own agents or your "
|
|
182
|
+
f"department defaults ({visible} visible to you)")
|
|
183
|
+
else:
|
|
184
|
+
# A source went unread, so absence was never established. Do NOT
|
|
185
|
+
# cache it: a three-minute outage must not become a
|
|
186
|
+
# process-lifetime "this agent does not exist". The per-source
|
|
187
|
+
# reasons are already recorded by _load_agent_index.
|
|
188
|
+
_note(f"'{aid}' could not be looked up")
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
row, source = entry
|
|
192
|
+
if not source:
|
|
193
|
+
_note(f"no agent with id '{aid}' is in your own agents or your department defaults")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
name = str(row.get("name") or "").strip() or aid
|
|
197
|
+
if _is_deleted(row):
|
|
198
|
+
_note(f"agent '{name}' (id {aid}) has been deleted")
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
status = str(row.get("status") or "").strip()
|
|
202
|
+
if status.lower() != "active":
|
|
203
|
+
# Strict, like _AgentRow.is_active and agentd._load_department_agents —
|
|
204
|
+
# NOT scheduler._agent_is_active's lenient "blank means active" variant,
|
|
205
|
+
# which exists to keep schedules alive through a backend hiccup. Nothing
|
|
206
|
+
# is guessed on the path that hands an agent its secrets.
|
|
207
|
+
_note(f"agent '{name}' (id {aid}) is not active (status={status or 'unknown'})")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
manifest = manifest_from_backend_row(row)
|
|
211
|
+
if manifest is None:
|
|
212
|
+
_note(f"agent '{name}' (id {aid}) has no usable prompt, so there is "
|
|
213
|
+
f"no instruction to run")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
return ResolvedAgent(agent_id=aid, manifest=manifest, source=source)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def resolve_manifest(
|
|
220
|
+
client: "BackendClient", name: str, scope: str = "custom", department_id: str = "",
|
|
221
|
+
reasons: list[str] | None = None,
|
|
222
|
+
) -> "AgentManifest | None":
|
|
223
|
+
"""Find the agent manifest to run, by ``scope``.
|
|
224
|
+
|
|
225
|
+
* ``department`` — an ACTIVE department default pulled from the backend
|
|
226
|
+
(``list_department_agents``) and materialized read-only (never written back).
|
|
227
|
+
* ``custom`` (default) — a local manifest, then the caller's own backend
|
|
228
|
+
agents, then their department defaults, via ``chat.resolve_agent``.
|
|
229
|
+
|
|
230
|
+
Backend rows are prompt-based (``prompt.system_prompt`` + attached skills);
|
|
231
|
+
the ``manifest`` column this used to read no longer exists on the table, so
|
|
232
|
+
reading it raised ``KeyError`` and made every department agent unrunnable.
|
|
233
|
+
|
|
234
|
+
Returns None when no matching agent is found, so the caller reports it. Pass
|
|
235
|
+
``reasons`` — a list this appends to — to learn WHY: "not found" and "your
|
|
236
|
+
token is not valid so the agent list could not be read" and "the agent exists
|
|
237
|
+
but has no prompt" are three very different problems that all end in None, and
|
|
238
|
+
a bare "was not found" sends the user hunting for a typo that isn't there.
|
|
239
|
+
"""
|
|
240
|
+
from sdk.agents.chat import resolve_agent
|
|
241
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
242
|
+
|
|
243
|
+
def _note(reason: str) -> None:
|
|
244
|
+
if reasons is not None:
|
|
245
|
+
reasons.append(reason)
|
|
246
|
+
|
|
247
|
+
name = (name or "").strip()
|
|
248
|
+
if scope == "department":
|
|
249
|
+
try:
|
|
250
|
+
rows = await client.list_department_agents()
|
|
251
|
+
except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
|
|
252
|
+
# str() is EMPTY on some httpx errors (ReadTimeout) — name the class then.
|
|
253
|
+
_note(f"department defaults could not be fetched "
|
|
254
|
+
f"({str(exc) or type(exc).__name__})")
|
|
255
|
+
return None
|
|
256
|
+
for row in rows:
|
|
257
|
+
if row.get("name") == name and row.get("status") == "active":
|
|
258
|
+
manifest = manifest_from_backend_row(row)
|
|
259
|
+
if manifest is None:
|
|
260
|
+
_note(f"'{name}' is a department default but its prompt is empty, "
|
|
261
|
+
f"so there is no instruction to run")
|
|
262
|
+
return manifest
|
|
263
|
+
_note(f"no ACTIVE department default named '{name}' "
|
|
264
|
+
f"({len(rows)} department agent(s) visible to you)")
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
return await resolve_agent(name, client)
|
|
269
|
+
except LookupError as exc:
|
|
270
|
+
# resolve_agent names every source it checked and why each one missed —
|
|
271
|
+
# keep that, it is the whole diagnosis.
|
|
272
|
+
_note(str(exc))
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def run_on_ticket(
|
|
277
|
+
client: "BackendClient", name: str, ticket_key: str = "",
|
|
278
|
+
scope: str = "custom", department_id: str = "",
|
|
279
|
+
variables: dict | None = None, secrets: dict | None = None,
|
|
280
|
+
) -> dict:
|
|
281
|
+
"""Run agent ``name`` (``scope``) now, OPTIONALLY against a Jira ticket.
|
|
282
|
+
|
|
283
|
+
* ``ticket_key`` — optional. When given, the ticket record is fetched (any
|
|
284
|
+
board status — a targeted run may re-run a Done ticket) and its fields fill
|
|
285
|
+
the agent's ``{{variables}}``. When omitted, the agent runs without a ticket.
|
|
286
|
+
* ``variables`` — optional EPHEMERAL ``{{var}}`` values for this run; they win
|
|
287
|
+
over ticket-derived values and are never persisted.
|
|
288
|
+
* ``secrets`` — optional EPHEMERAL env secrets for this run only; injected for
|
|
289
|
+
the engine invocation and never written to the agent's stored keys.
|
|
290
|
+
|
|
291
|
+
Resolves the manifest and dispatches through the shared pipeline with
|
|
292
|
+
``auto_approve=True``. Returns the dispatch outcome dict
|
|
293
|
+
(``{status, task_id, result, ok, error}``).
|
|
294
|
+
|
|
295
|
+
Raises ``ValueError`` when the agent — or an explicitly-named ticket — cannot
|
|
296
|
+
be found, so the caller reports a clear message. For the agent, that message
|
|
297
|
+
carries the resolver's REASON (unreadable agent list vs. genuinely absent vs.
|
|
298
|
+
present-but-promptless): they demand different fixes and used to be
|
|
299
|
+
indistinguishable behind one "was not found".
|
|
300
|
+
"""
|
|
301
|
+
from sdk.agentd import _dispatch_agent_run
|
|
302
|
+
|
|
303
|
+
key = (ticket_key or "").strip()
|
|
304
|
+
task: dict | None = None
|
|
305
|
+
if key:
|
|
306
|
+
records = await client.list_tasks_by_keys([key])
|
|
307
|
+
if not records:
|
|
308
|
+
raise ValueError(f"Jira ticket '{key}' was not found.")
|
|
309
|
+
task = records[0]
|
|
310
|
+
|
|
311
|
+
reasons: list[str] = []
|
|
312
|
+
manifest = await resolve_manifest(
|
|
313
|
+
client, name, scope=scope, department_id=department_id, reasons=reasons,
|
|
314
|
+
)
|
|
315
|
+
if manifest is None:
|
|
316
|
+
detail = " — ".join(reasons)
|
|
317
|
+
raise ValueError(
|
|
318
|
+
f"Agent '{name}' could not be run (scope={scope})"
|
|
319
|
+
+ (f" — {detail}." if detail else ".")
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return await _dispatch_agent_run(
|
|
323
|
+
manifest, client, task, auto_approve=True,
|
|
324
|
+
extra_variables=variables or None, extra_secrets=secrets or None,
|
|
325
|
+
)
|