devops-bot-sdk 1.4.120__tar.gz → 1.4.125__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.
Files changed (89) hide show
  1. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/inventory.py +45 -33
  5. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/api.py +72 -52
  6. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/app.js +39 -8
  7. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/custom-agents.html +6 -3
  8. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/README.md +0 -0
  9. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  10. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  11. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  12. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/requires.txt +0 -0
  13. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  14. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/pyproject.toml +0 -0
  15. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agentd.py +0 -0
  16. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/__init__.py +0 -0
  17. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/builder.py +0 -0
  18. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/chat.py +0 -0
  19. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/connectors.py +0 -0
  20. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/__init__.py +0 -0
  21. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/claude_code.py +0 -0
  22. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/http.py +0 -0
  23. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/mcp.py +0 -0
  24. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/n8n.py +0 -0
  25. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/engines/shell.py +0 -0
  26. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/integrations.py +0 -0
  27. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/library/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/manifest.py +0 -0
  29. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/prompt_studio.py +0 -0
  30. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/prompt_template.py +0 -0
  31. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/prompt_vars.py +0 -0
  32. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/run_log.py +0 -0
  33. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/secrets.py +0 -0
  34. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/usage.py +0 -0
  35. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/agents/varstore.py +0 -0
  36. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/cli.py +0 -0
  37. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/client.py +0 -0
  38. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/collectors/__init__.py +0 -0
  39. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/collectors/files.py +0 -0
  40. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/collectors/process.py +0 -0
  41. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/collectors/screenshot.py +0 -0
  42. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/config.py +0 -0
  43. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/crucial.py +0 -0
  44. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/__init__.py +0 -0
  45. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/assets.py +0 -0
  46. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/bootstrap.py +0 -0
  47. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/browser.py +0 -0
  48. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/compare.py +0 -0
  49. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/design_verify/loop.py +0 -0
  50. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/exceptions.py +0 -0
  51. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/git_ops.py +0 -0
  52. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/graphify.py +0 -0
  53. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/hooks/__init__.py +0 -0
  54. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/hooks/crucial_guard.py +0 -0
  55. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ipc/__init__.py +0 -0
  56. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ipc/electron_bridge.py +0 -0
  57. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ipc/handlers.py +0 -0
  58. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/local_exec.py +0 -0
  59. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/models/__init__.py +0 -0
  60. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/models/envelope.py +0 -0
  61. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/models/requests.py +0 -0
  62. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/models/responses.py +0 -0
  63. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/models/snapshots.py +0 -0
  64. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/platform_compat.py +0 -0
  65. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/py.typed +0 -0
  66. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/run_auto.py +0 -0
  67. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/sse.py +0 -0
  68. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/test.py +0 -0
  69. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/test_pipeline.py +0 -0
  70. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/__init__.py +0 -0
  71. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/dashboard.py +0 -0
  72. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/ip_allowlist.py +0 -0
  73. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/jobs.py +0 -0
  74. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/runner.py +0 -0
  75. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/security.py +0 -0
  76. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/server.py +0 -0
  77. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/service.py +0 -0
  78. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/setup_manager.py +0 -0
  79. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/agents.html +0 -0
  80. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/default-agents.html +0 -0
  81. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/index.html +0 -0
  82. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/runs.html +0 -0
  83. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/setup.html +0 -0
  84. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/styles.css +0 -0
  85. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/ui/static/tickets.html +0 -0
  86. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/sdk/updater.py +0 -0
  87. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/setup.cfg +0 -0
  88. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/tests/test_agent_chat_features.py +0 -0
  89. {devops_bot_sdk-1.4.120 → devops_bot_sdk-1.4.125}/tests/test_agents_connectors_builder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.120
3
+ Version: 1.4.125
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.120
3
+ Version: 1.4.125
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.120
3
+ Version: 1.4.125
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.120" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.125" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -16,22 +16,39 @@ import subprocess
16
16
  from pathlib import Path
17
17
 
18
18
  from sdk.agents import usage as usage_mod
19
- from sdk.agents.manifest import load_agents, load_skills
20
- from sdk.agents.prompt_vars import extract_variables
19
+ from sdk.agents.manifest import load_skills
21
20
 
22
21
  logger = logging.getLogger("sdk.agents.inventory")
23
22
 
24
23
 
25
- def _custom_agents() -> list[dict]:
26
- out: list[dict] = []
27
- for m in load_agents():
28
- out.append({
29
- "name": m.name, "engine": m.engine, "trigger": m.trigger.type,
30
- "description": m.description, "integrations": list(m.integrations),
31
- "skills": list(m.skills), "variables": extract_variables(m.prompt),
32
- "source": "custom",
33
- })
34
- return out
24
+ def _map_backend_agent(row: dict, source: str) -> dict:
25
+ """Project a backend prompt-based ``agents`` row into the inventory shape."""
26
+ prompt = row.get("prompt") or {}
27
+ sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
28
+ pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
29
+ return {
30
+ "name": row.get("name"),
31
+ "description": sys_prompt[:200],
32
+ "variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
33
+ "skills": row.get("skills", []),
34
+ "status": row.get("status", "active"),
35
+ "source": source,
36
+ }
37
+
38
+
39
+ async def _custom_agents() -> list[dict]:
40
+ """The caller's custom agents (``relation_type=person``) from the backend.
41
+
42
+ Backend is the source of truth — agents are authored in the web app and read
43
+ via GET /api/v1/agents. Best-effort: degrades to empty on any error.
44
+ """
45
+ try:
46
+ from sdk.client import BackendClient
47
+ rows = await BackendClient.from_config().list_custom_agents()
48
+ return [_map_backend_agent(r, "custom") for r in rows]
49
+ except Exception as exc: # noqa: BLE001
50
+ logger.debug("custom-agent inventory failed: %s", exc)
51
+ return []
35
52
 
36
53
 
37
54
  def _skills() -> list[dict]:
@@ -101,31 +118,26 @@ def _claude_subagents() -> list[dict]:
101
118
 
102
119
 
103
120
  async def _default_agents() -> list[dict]:
104
- """Department default agents from the bundled SDK library (best-effort).
121
+ """Department default agents (``relation_type=department`` + org-wide common)
122
+ from the backend (best-effort).
105
123
 
106
- Resolves the caller's department by *name* (the ``title`` on the backend
107
- profile, e.g. "Machine Learning") to a library slug and loads that
108
- department's templates locally — no AI-backend ``/departments/{id}/
109
- default-agents`` call.
124
+ Reads the live rows via GET /api/v1/department-agents — the source of truth —
125
+ rather than the bundled SDK library. Degrades to empty on any error.
110
126
  """
111
127
  try:
112
128
  from sdk.client import BackendClient
113
- from sdk.agents.library import (
114
- load_common_templates, load_department, resolve_department_slug,
115
- )
116
-
117
129
  client = BackendClient.from_config()
118
- profile = await client.profile()
119
- title = profile.department_title or ""
120
- slug = resolve_department_slug(title)
121
- # A resolved department already includes the common agents; a department
122
- # with no library entry still surfaces the common defaults.
123
- templates = load_department(slug) if slug else load_common_templates(title=title)
124
- return [{"name": t.manifest.name, "engine": t.manifest.engine,
125
- "description": t.manifest.description, "source": "default",
126
- "department": title,
127
- "department_id": profile.department_id,
128
- "min_role_tier": t.min_role_tier} for t in templates]
130
+ try:
131
+ title = (await client.profile()).department_title or ""
132
+ except Exception: # noqa: BLE001 — department label is cosmetic
133
+ title = ""
134
+ rows = await client.list_department_agents()
135
+ out = []
136
+ for r in rows:
137
+ entry = _map_backend_agent(r, "default")
138
+ entry["department"] = title
139
+ out.append(entry)
140
+ return out
129
141
  except Exception as exc: # noqa: BLE001
130
142
  logger.debug("default-agent inventory failed: %s", exc)
131
143
  return []
@@ -133,7 +145,7 @@ async def _default_agents() -> list[dict]:
133
145
 
134
146
  async def build_inventory() -> dict:
135
147
  """The full snapshot: agents, subagents, tools, plugins + per-agent usage."""
136
- customs = _custom_agents()
148
+ customs = await _custom_agents()
137
149
  usage = usage_mod.summary()
138
150
  # Attach utilisation to each custom agent.
139
151
  for a in customs:
@@ -265,75 +265,95 @@ async def connectors() -> dict:
265
265
 
266
266
  @router.get("/agents", dependencies=[_auth])
267
267
  async def agents() -> dict:
268
- """Locally-installed agents (name, engine, trigger, integrations, variables)."""
269
- from sdk.agents.manifest import load_agents
270
- from sdk.agents.prompt_vars import extract_variables
268
+ """The caller's custom agents (``relation_type=person``) from the backend.
271
269
 
272
- return {
273
- "agents": [
274
- {"name": m.name, "engine": m.engine, "trigger": m.trigger.type,
275
- "integrations": m.integrations, "description": m.description,
276
- "variables": extract_variables(m.prompt)}
277
- for m in load_agents()
278
- ]
270
+ Backend is the source of truth — agents are authored in the web app and read
271
+ here via GET /api/v1/agents (prompt-based shape: prompt = {system_prompt,
272
+ variables:[…]}; skills = [{name,…}]). Best-effort: on backend error the list
273
+ is empty with an ``error`` string the UI can surface.
274
+ """
275
+ from sdk.client import BackendClient
276
+ from sdk.exceptions import BackendFeatureMissing
277
+
278
+ try:
279
+ rows = await BackendClient.from_config().list_custom_agents()
280
+ except BackendFeatureMissing:
281
+ return {"agents": [], "error": ("Custom agents aren't available on this "
282
+ "backend — it may be running an older version.")}
283
+ except Exception as exc: # noqa: BLE001 — degrade to empty, surface the reason
284
+ return {"agents": [], "error": str(exc)}
285
+ return {"agents": rows, "error": ""}
286
+
287
+
288
+ def _flat_agent(row: dict, source: str, *, chattable: bool, department_id: str = "") -> dict:
289
+ """Flatten a backend prompt-based ``agents`` row into the picker shape the UI
290
+ expects (``name``/``description``/``variables``/``skills``/``source``)."""
291
+ prompt = row.get("prompt") or {}
292
+ sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
293
+ pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
294
+ entry = {
295
+ "name": row.get("name"),
296
+ "description": sys_prompt[:200],
297
+ "variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
298
+ "skills": row.get("skills", []),
299
+ "status": row.get("status", "active"),
300
+ "source": source,
301
+ "chattable": chattable,
279
302
  }
303
+ if department_id:
304
+ entry["department_id"] = department_id
305
+ return entry
280
306
 
281
307
 
282
308
  @router.get("/agents/all", dependencies=[_auth])
283
309
  async def agents_all(department_id: str = Query("")) -> dict:
284
- """Every agent a person can pick to chat: local custom + department defaults.
285
-
286
- Custom agents are chattable immediately; a default must be activated (cloned
287
- locally) first — surfaced via ``chattable`` and ``source``. Each entry carries
288
- its detected ``{{variables}}`` so the UI can render input fields. Defaults are
289
- best-effort (empty if the backend is unreachable).
310
+ """Every agent a person can pick to chat: their custom agents + department
311
+ defaults, both read from the backend (source of truth).
312
+
313
+ Custom agents (``relation_type=person``) come from GET /api/v1/agents; defaults
314
+ (``relation_type=department`` + org-wide common) from GET /api/v1/department-agents.
315
+ Custom wins on a name collision. Each entry carries its detected variables so
316
+ the UI can render input fields. Both are best-effort — an unreachable backend
317
+ yields an empty list plus an error string rather than a hard failure.
290
318
  """
291
- from sdk.agents.manifest import load_agents
292
- from sdk.agents.prompt_vars import extract_variables
293
-
294
- locals_ = load_agents()
295
- local_names = {m.name for m in locals_}
296
- out = [
297
- {"name": m.name, "engine": m.engine, "description": m.description,
298
- "variables": extract_variables(m.prompt), "source": "custom",
299
- "chattable": True}
300
- for m in locals_
301
- ]
319
+ from sdk.client import BackendClient
320
+ from sdk.exceptions import BackendFeatureMissing
302
321
 
322
+ out: list[dict] = []
323
+ seen: set[str] = set()
303
324
  dept = department_id
304
325
  defaults_error = ""
326
+
305
327
  try:
306
- from sdk.client import BackendClient
307
- from sdk.exceptions import BackendFeatureMissing
308
328
  client = BackendClient.from_config()
309
329
  if not dept:
310
330
  profile = await client.bootstrap()
311
331
  dept = getattr(profile, "department_id", None) or ""
312
- if dept:
313
- try:
314
- # Department defaults are now plain `agents` rows (relation_type=
315
- # department), read from /department-agents in the new prompt-based
316
- # schema: prompt = {system_prompt, variables:[{name,...}]}.
317
- rows = await client.list_department_agents()
318
- except BackendFeatureMissing:
319
- rows = []
320
- defaults_error = ("Department default agents aren't available on this "
321
- "backend — it may be running an older version.")
322
- for r in rows:
332
+
333
+ # Custom agents first so they win any name collision with a default.
334
+ try:
335
+ for r in await client.list_custom_agents():
336
+ nm = r.get("name")
337
+ if not nm or nm in seen:
338
+ continue
339
+ seen.add(nm)
340
+ out.append(_flat_agent(r, "custom", chattable=True))
341
+ except BackendFeatureMissing:
342
+ defaults_error = ("Custom agents aren't available on this backend — "
343
+ "it may be running an older version.")
344
+
345
+ # Department defaults (a default must be activated/cloned before it runs).
346
+ try:
347
+ for r in await client.list_department_agents():
323
348
  nm = r.get("name")
324
- if not nm or nm in local_names:
349
+ if not nm or nm in seen:
325
350
  continue
326
- prompt = r.get("prompt") or {}
327
- sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
328
- pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
329
- out.append({
330
- "name": nm,
331
- "description": sys_prompt[:200],
332
- "variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
333
- "skills": r.get("skills", []),
334
- "source": "default", "department_id": dept, "chattable": False,
335
- })
336
- except Exception: # noqa: BLE001 — defaults are best-effort
351
+ seen.add(nm)
352
+ out.append(_flat_agent(r, "default", chattable=False, department_id=dept))
353
+ except BackendFeatureMissing:
354
+ defaults_error = ("Department default agents aren't available on this "
355
+ "backend — it may be running an older version.")
356
+ except Exception: # noqa: BLE001 — best-effort; return whatever we gathered
337
357
  pass
338
358
 
339
359
  return {"agents": out, "department_id": dept, "defaults_error": defaults_error}
@@ -650,26 +650,40 @@
650
650
  try {
651
651
  const d = await api("/api/ui/agents");
652
652
  const rows = d.agents || [];
653
+ if (!rows.length && d.error) {
654
+ $("local-agents").innerHTML = `<div class="empty">${esc(d.error)}</div>`;
655
+ const kaSel0 = $("key-agent");
656
+ if (kaSel0) { kaSel0.innerHTML = `<option value="">(no agents yet)</option>`; onAgentSelect(); }
657
+ return;
658
+ }
653
659
  caMeta = {}; rows.forEach(a => { caMeta[a.name] = a; });
654
660
  const kaSel = $("key-agent");
655
661
  if (kaSel) {
656
662
  kaSel.innerHTML = rows.length
657
- ? rows.map(a => `<option value="${esc(a.name)}">${esc(a.name)} (${esc(a.engine)})</option>`).join("")
663
+ ? rows.map(a => `<option value="${esc(a.name)}">${esc(a.name)}</option>`).join("")
658
664
  : `<option value="">(no agents yet)</option>`;
659
665
  onAgentSelect();
660
666
  }
661
- $("local-agents").innerHTML = rows.length ? rows.map(a =>
662
- `<div class="agent-card">
663
- <div class="agent-head"><span class="agent-name">${esc(a.name)}</span><span class="pill on">${esc(a.engine)}</span></div>
664
- <div class="agent-meta">${esc(a.trigger)}${a.integrations.length ? " · " + a.integrations.map(esc).join(", ") : ""}${a.variables && a.variables.length ? " · " + a.variables.length + " input(s)" : ""}</div>
665
- <div class="agent-desc">${esc(a.description || "")}</div>
667
+ $("local-agents").innerHTML = rows.length ? rows.map(a => {
668
+ const prompt = a.prompt || {};
669
+ const summary = (prompt.system_prompt || "").trim().slice(0, 200);
670
+ const nvars = (prompt.variables || []).length;
671
+ const nskills = (a.skills || []).length;
672
+ return `<div class="agent-card">
673
+ <div class="agent-head">
674
+ <span class="agent-name">${esc(a.name)}</span>
675
+ <span class="pill ${a.status === "disabled" ? "off" : "on"}">${esc(a.status || "active")}</span>
676
+ </div>
677
+ <div class="agent-meta">${nvars} variable(s) · ${nskills} skill(s)</div>
678
+ <div class="agent-desc">${esc(summary)}</div>
666
679
  <div class="agent-actions">
667
680
  <a class="btn primary" href="/agents.html?agent=${encodeURIComponent(a.name)}">Run</a>
668
681
  <button class="btn" data-enable="${esc(a.name)}">Enable</button>
669
682
  <button class="btn" data-disable="${esc(a.name)}">Disable</button>
670
683
  <button class="btn danger" data-del="${esc(a.name)}">Delete</button>
671
684
  </div>
672
- </div>`).join("")
685
+ </div>`;
686
+ }).join("")
673
687
  : "<div class='empty'>No custom agents assigned to you yet. Agents are created in the web app.</div>";
674
688
 
675
689
  $("local-agents").querySelectorAll("[data-enable]").forEach(b => b.onclick = () => setStatus(b.dataset.enable, "active"));
@@ -734,11 +748,20 @@
734
748
  return out;
735
749
  }
736
750
 
751
+ function collapseKeyAdd() {
752
+ const box = $("key-add"); if (!box) return;
753
+ box.style.display = "none";
754
+ if ($("key-name")) $("key-name").value = "";
755
+ if ($("key-value")) $("key-value").value = "";
756
+ if ($("key-add-toggle")) $("key-add-toggle").textContent = "+ Add API key";
757
+ }
758
+
737
759
  function onAgentSelect() {
738
760
  const m = caMeta[$("key-agent") && $("key-agent").value];
739
761
  if ($("ca-about")) $("ca-about").textContent = m ? (m.description || "No description provided.") : "Pick an agent.";
740
762
  if ($("ca-output")) $("ca-output").style.display = "none";
741
763
  if ($("ca-run-status")) $("ca-run-status").textContent = "";
764
+ collapseKeyAdd();
742
765
  loadVars();
743
766
  loadKeys();
744
767
  }
@@ -772,6 +795,14 @@
772
795
  }
773
796
  }
774
797
 
798
+ if ($("key-add-toggle")) $("key-add-toggle").onclick = () => {
799
+ const box = $("key-add"); if (!box) return;
800
+ const show = box.style.display === "none";
801
+ box.style.display = show ? "" : "none";
802
+ $("key-add-toggle").textContent = show ? "Cancel" : "+ Add API key";
803
+ if (show && $("key-name")) $("key-name").focus();
804
+ };
805
+
775
806
  if ($("key-agent")) $("key-agent").addEventListener("change", onAgentSelect);
776
807
  if ($("ca-run")) $("ca-run").onclick = caRun;
777
808
  if ($("ca-stop")) $("ca-stop").onclick = () => { if (caRunJob) { cancelJob(caRunJob); toast("Stopping…"); } };
@@ -782,7 +813,7 @@
782
813
  if (!v) { toast("Enter a value", "err"); return; }
783
814
  try {
784
815
  await api("/api/ui/agents/keys", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, keys: { [k]: v } }) });
785
- $("key-name").value = ""; $("key-value").value = "";
816
+ collapseKeyAdd();
786
817
  toast("Saved " + k + " for " + name, "ok"); loadKeys();
787
818
  } catch (e) { toast("Save failed: " + (e.message || ""), "err"); }
788
819
  };
@@ -52,9 +52,12 @@
52
52
  Leave blank to keep the current one; enter a value to change it.
53
53
  </p>
54
54
  <div id="key-list" class="vars"><div class="muted">Pick an agent to see its stored keys.</div></div>
55
- <div class="field"><label>Key name (e.g. OPENAI_API_KEY)</label><input id="key-name" placeholder="ENV_VAR_NAME" autocomplete="off" /></div>
56
- <div class="field"><label>Value</label><input id="key-value" type="password" placeholder="paste the secret (hidden)" autocomplete="off" /></div>
57
- <button class="btn" id="key-save">Save key</button>
55
+ <button class="btn sm" id="key-add-toggle" style="margin-top:.4em">+ Add API key</button>
56
+ <div id="key-add" style="display:none;margin-top:.5em">
57
+ <div class="field"><label>Key name (e.g. OPENAI_API_KEY)</label><input id="key-name" placeholder="ENV_VAR_NAME" autocomplete="off" /></div>
58
+ <div class="field"><label>Value</label><input id="key-value" type="password" placeholder="paste the secret (hidden)" autocomplete="off" /></div>
59
+ <button class="btn" id="key-save">Save key</button>
60
+ </div>
58
61
  </div>
59
62
  </div>
60
63
  </div>