glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__py3-none-any.whl

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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,116 @@
1
+ """Agent CLI commands package.
2
+
3
+ This package contains agent management commands split by operation.
4
+ The package is the canonical import surface.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ # pylint: disable=duplicate-code
11
+ # Import from submodules
12
+ from glaip_sdk.cli.commands.agents._common import ( # noqa: E402
13
+ AGENT_NOT_FOUND_ERROR,
14
+ _coerce_mapping_candidate,
15
+ _display_agent_details,
16
+ _emit_verbose_guidance,
17
+ _fetch_full_agent_details,
18
+ _get_agent_for_update,
19
+ _get_agent_model_name,
20
+ _get_language_model_display_name,
21
+ _model_from_config,
22
+ _prepare_agent_output,
23
+ _resolve_agent,
24
+ _resolve_resources_by_name,
25
+ agents_group,
26
+ console,
27
+ )
28
+ from glaip_sdk.cli.commands.agents.create import create # noqa: E402
29
+ from glaip_sdk.cli.commands.agents.delete import delete # noqa: E402
30
+ from glaip_sdk.cli.commands.agents.get import get # noqa: E402
31
+ from glaip_sdk.cli.commands.agents.list import list_agents # noqa: E402
32
+ from glaip_sdk.cli.commands.agents.run import _maybe_attach_transcript_toggle, run # noqa: E402
33
+ from glaip_sdk.cli.commands.agents.sync_langflow import sync_langflow # noqa: E402
34
+ from glaip_sdk.cli.commands.agents.update import update # noqa: E402
35
+
36
+ # Import core functions for test compatibility
37
+ from glaip_sdk.cli.core.context import get_client # noqa: E402
38
+
39
+ # Import core output functions for test compatibility
40
+ from glaip_sdk.cli.core.output import ( # noqa: E402
41
+ handle_resource_export,
42
+ output_list,
43
+ )
44
+
45
+ # Import rendering functions for test compatibility
46
+ from glaip_sdk.cli.core.rendering import ( # noqa: E402
47
+ build_renderer,
48
+ with_client_and_spinner, # noqa: E402
49
+ )
50
+
51
+ # Import display functions for test compatibility
52
+ # Import display functions for test compatibility
53
+ from glaip_sdk.cli.display import ( # noqa: E402 # noqa: E402
54
+ display_agent_run_suggestions,
55
+ handle_json_output,
56
+ handle_rich_output,
57
+ )
58
+
59
+ # Import IO functions for test compatibility
60
+ from glaip_sdk.cli.io import ( # noqa: E402
61
+ fetch_raw_resource_details,
62
+ )
63
+
64
+ # Import rich helpers for test compatibility
65
+ from glaip_sdk.cli.rich_helpers import ( # noqa: E402
66
+ markup_text,
67
+ )
68
+
69
+ # Import transcript functions for test compatibility
70
+ from glaip_sdk.cli.transcript import ( # noqa: E402
71
+ maybe_launch_post_run_viewer,
72
+ store_transcript_for_session,
73
+ )
74
+
75
+ # Import utils for test compatibility
76
+ from glaip_sdk.utils import ( # noqa: E402
77
+ is_uuid,
78
+ )
79
+
80
+ __all__ = [
81
+ "AGENT_NOT_FOUND_ERROR",
82
+ "agents_group",
83
+ "create",
84
+ "delete",
85
+ "get",
86
+ "list_agents",
87
+ "run",
88
+ "sync_langflow",
89
+ "update",
90
+ "_get_agent_for_update",
91
+ "_resolve_agent",
92
+ "_coerce_mapping_candidate",
93
+ "_display_agent_details",
94
+ "_emit_verbose_guidance",
95
+ "_fetch_full_agent_details",
96
+ "_get_agent_model_name",
97
+ "_get_language_model_display_name",
98
+ "_model_from_config",
99
+ "_prepare_agent_output",
100
+ "_resolve_resources_by_name",
101
+ "_maybe_attach_transcript_toggle",
102
+ "get_client",
103
+ "with_client_and_spinner",
104
+ "console",
105
+ "handle_json_output",
106
+ "handle_rich_output",
107
+ "output_list",
108
+ "handle_resource_export",
109
+ "build_renderer",
110
+ "display_agent_run_suggestions",
111
+ "markup_text",
112
+ "maybe_launch_post_run_viewer",
113
+ "store_transcript_for_session",
114
+ "fetch_raw_resource_details",
115
+ "is_uuid",
116
+ ]
@@ -0,0 +1,562 @@
1
+ """Common helpers and group definition for agent commands.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from copy import deepcopy
11
+ from typing import Any
12
+
13
+ import click
14
+ from rich.console import Console
15
+
16
+ from glaip_sdk.branding import (
17
+ ERROR_STYLE,
18
+ HINT_PREFIX_STYLE,
19
+ WARNING_STYLE,
20
+ )
21
+ from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
22
+ from glaip_sdk.cli.context import get_ctx_value
23
+ from glaip_sdk.cli.core.output import (
24
+ output_result,
25
+ )
26
+ from glaip_sdk.cli.core.rendering import spinner_context
27
+ from glaip_sdk.cli.display import (
28
+ build_resource_result_data,
29
+ handle_json_output,
30
+ handle_rich_output,
31
+ print_api_error,
32
+ )
33
+ from glaip_sdk.cli.hints import in_slash_mode
34
+ from glaip_sdk.cli.io import fetch_raw_resource_details
35
+ from glaip_sdk.cli.resolution import resolve_resource_reference
36
+ from glaip_sdk.cli.rich_helpers import markup_text
37
+ from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS
38
+ from glaip_sdk.models.constants import DEFAULT_MODEL
39
+ from glaip_sdk.icons import ICON_AGENT
40
+ from glaip_sdk.utils import format_datetime, is_uuid
41
+
42
+ console = Console()
43
+
44
+ # Error message constants
45
+ AGENT_NOT_FOUND_ERROR = "Agent not found"
46
+
47
+
48
+ def _safe_agent_attribute(agent: Any, name: str) -> Any:
49
+ """Return attribute value for ``name`` while filtering Mock sentinels."""
50
+ try:
51
+ value = getattr(agent, name)
52
+ except Exception:
53
+ return None
54
+
55
+ if hasattr(value, "_mock_name"):
56
+ return None
57
+ return value
58
+
59
+
60
+ def _coerce_mapping_candidate(candidate: Any) -> dict[str, Any] | None:
61
+ """Convert a mapping-like candidate to a plain dict when possible."""
62
+ if candidate is None:
63
+ return None
64
+ if isinstance(candidate, Mapping):
65
+ return dict(candidate)
66
+ return None
67
+
68
+
69
+ def _call_agent_method(agent: Any, method_name: str) -> dict[str, Any] | None:
70
+ """Attempt to call the named method and coerce its output to a dict."""
71
+ method = getattr(agent, method_name, None)
72
+ if not callable(method):
73
+ return None
74
+ try:
75
+ candidate = method()
76
+ except Exception:
77
+ return None
78
+ return _coerce_mapping_candidate(candidate)
79
+
80
+
81
+ def _coerce_agent_via_methods(agent: Any) -> dict[str, Any] | None:
82
+ """Try standard serialisation helpers to produce a mapping."""
83
+ for attr in ("model_dump", "dict", "to_dict"):
84
+ mapping = _call_agent_method(agent, attr)
85
+ if mapping is not None:
86
+ return mapping
87
+ return None
88
+
89
+
90
+ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
91
+ """Construct a minimal mapping from well-known agent attributes."""
92
+ fallback_fields = (
93
+ "id",
94
+ "name",
95
+ "instruction",
96
+ "description",
97
+ "model",
98
+ "agent_config",
99
+ *[field for field in AGENT_CONFIG_FIELDS if field not in ("name", "instruction", "model")],
100
+ "tool_configs",
101
+ )
102
+
103
+ fallback: dict[str, Any] = {}
104
+ for field in fallback_fields:
105
+ value = _safe_agent_attribute(agent, field)
106
+ if value is not None:
107
+ fallback[field] = value
108
+
109
+ return fallback or {"name": str(agent)}
110
+
111
+
112
+ def _prepare_agent_output(agent: Any) -> dict[str, Any]:
113
+ """Build a JSON-serialisable mapping for CLI output."""
114
+ method_mapping = _coerce_agent_via_methods(agent)
115
+ if method_mapping is not None:
116
+ return method_mapping
117
+
118
+ intrinsic = _coerce_mapping_candidate(agent)
119
+ if intrinsic is not None:
120
+ return intrinsic
121
+
122
+ return _build_fallback_agent_mapping(agent)
123
+
124
+
125
+ def _fetch_full_agent_details(client: Any, agent: Any) -> Any | None:
126
+ """Fetch full agent details by ID to ensure all fields are populated."""
127
+ try:
128
+ agent_id = str(getattr(agent, "id", "")).strip()
129
+ if agent_id:
130
+ return client.agents.get_agent_by_id(agent_id)
131
+ except Exception:
132
+ # If fetching full details fails, continue with the resolved object
133
+ pass
134
+ return agent
135
+
136
+
137
+ def _normalise_model_name(value: Any) -> str | None:
138
+ """Return a cleaned model name or None when not usable."""
139
+ if value is None:
140
+ return None
141
+ if isinstance(value, str):
142
+ cleaned = value.strip()
143
+ return cleaned or None
144
+ if isinstance(value, bool):
145
+ return None
146
+ return str(value)
147
+
148
+
149
+ def _model_from_config(agent: Any) -> str | None:
150
+ """Extract a usable model name from an agent's configuration mapping."""
151
+ config = getattr(agent, "agent_config", None)
152
+ if not config or not isinstance(config, dict):
153
+ return None
154
+
155
+ for key in ("lm_name", "model"):
156
+ normalised = _normalise_model_name(config.get(key))
157
+ if normalised:
158
+ return normalised
159
+ return None
160
+
161
+
162
+ def _get_agent_model_name(agent: Any) -> str | None:
163
+ """Extract model name from agent configuration."""
164
+ config_model = _model_from_config(agent)
165
+ if config_model:
166
+ return config_model
167
+
168
+ normalised_attr = _normalise_model_name(getattr(agent, "model", None))
169
+ if normalised_attr:
170
+ return normalised_attr
171
+
172
+ return DEFAULT_MODEL
173
+
174
+
175
+ def _resolve_resources_by_name(
176
+ _client: Any, items: tuple[str, ...], resource_type: str, find_func: Any, label: str
177
+ ) -> list[str]:
178
+ """Resolve resource names/IDs to IDs, handling ambiguity.
179
+
180
+ Args:
181
+ client: API client
182
+ items: Tuple of resource names/IDs
183
+ resource_type: Type of resource ("tool" or "agent")
184
+ find_func: Function to find resources by name
185
+ label: Label for error messages
186
+
187
+ Returns:
188
+ List of resolved resource IDs
189
+ """
190
+ out = []
191
+ for ref in items or ():
192
+ if is_uuid(ref):
193
+ out.append(ref)
194
+ continue
195
+
196
+ matches = find_func(name=ref)
197
+ if not matches:
198
+ raise click.ClickException(f"{label} not found: {ref}")
199
+ if len(matches) > 1:
200
+ raise click.ClickException(f"Multiple {resource_type}s named '{ref}'. Use ID instead.")
201
+ out.append(str(matches[0].id))
202
+ return out
203
+
204
+
205
+ def _split_comma_separated_refs(items: tuple[str, ...] | None) -> tuple[str, ...]:
206
+ """Expand comma-separated CLI values into a flat tuple.
207
+
208
+ Click ``multiple=True`` options can be provided as repeated flags (``--tools t1 --tools t2``)
209
+ or as a single comma-separated value (``--tools t1,t2``). Keep both forms working.
210
+ """
211
+ if not items:
212
+ return ()
213
+
214
+ resolved: list[str] = []
215
+ for item in items:
216
+ if item is None:
217
+ continue
218
+ for part in str(item).split(","):
219
+ cleaned = part.strip()
220
+ if cleaned:
221
+ resolved.append(cleaned)
222
+ return tuple(resolved)
223
+
224
+
225
+ def _fetch_and_format_raw_agent_data(client: Any, agent: Any) -> dict | None:
226
+ """Fetch raw agent data and format it for display."""
227
+ try:
228
+ raw_agent_data = fetch_raw_resource_details(client, agent, "agents")
229
+ if not raw_agent_data:
230
+ return None
231
+
232
+ # Format dates for better display
233
+ formatted_data = raw_agent_data.copy()
234
+ if "created_at" in formatted_data:
235
+ formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
236
+ if "updated_at" in formatted_data:
237
+ formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
238
+
239
+ return formatted_data
240
+ except Exception:
241
+ return None
242
+
243
+
244
+ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
245
+ """Format fallback agent data using Pydantic model."""
246
+ full_agent = _fetch_full_agent_details(client, agent)
247
+
248
+ # Define fields to extract
249
+ fields = [
250
+ "id",
251
+ "name",
252
+ "type",
253
+ "framework",
254
+ "version",
255
+ "description",
256
+ "instruction",
257
+ "created_at",
258
+ "updated_at",
259
+ "metadata",
260
+ "language_model_id",
261
+ "agent_config",
262
+ "tools",
263
+ "agents",
264
+ "mcps",
265
+ "a2a_profile",
266
+ "tool_configs",
267
+ ]
268
+
269
+ result_data = build_resource_result_data(full_agent, fields)
270
+
271
+ # Handle missing instruction
272
+ if result_data.get("instruction") in ["N/A", None, ""]:
273
+ result_data["instruction"] = "-"
274
+
275
+ # Format dates for better display
276
+ for date_field in ["created_at", "updated_at"]:
277
+ if result_data.get(date_field) and result_data[date_field] not in ["N/A", None]:
278
+ result_data[date_field] = format_datetime(result_data[date_field])
279
+
280
+ return result_data
281
+
282
+
283
+ def _clamp_instruction_preview_limit(limit: int | None) -> int:
284
+ """Normalise preview limit; 0 disables trimming."""
285
+ default = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
286
+ if limit is None: # pragma: no cover
287
+ return default
288
+ try:
289
+ limit_value = int(limit)
290
+ except (TypeError, ValueError): # pragma: no cover - defensive parsing
291
+ return default
292
+
293
+ if limit_value <= 0:
294
+ return 0
295
+
296
+ return limit_value
297
+
298
+
299
+ def _build_instruction_preview(value: Any, limit: int) -> tuple[Any, bool]:
300
+ """Return a trimmed preview for long instruction strings."""
301
+ if not isinstance(value, str) or limit <= 0: # pragma: no cover
302
+ return value, False
303
+
304
+ if len(value) <= limit:
305
+ return value, False
306
+
307
+ trimmed_value = value[:limit].rstrip()
308
+ preview = f"{trimmed_value}\n\n... (preview trimmed)"
309
+ return preview, True
310
+
311
+
312
+ def _prepare_agent_details_payload(
313
+ data: dict[str, Any],
314
+ *,
315
+ instruction_preview_limit: int,
316
+ ) -> tuple[dict[str, Any], bool]:
317
+ """Return payload ready for rendering plus trim indicator."""
318
+ payload = deepcopy(data)
319
+ trimmed = False
320
+ if instruction_preview_limit > 0:
321
+ preview, trimmed = _build_instruction_preview(payload.get("instruction"), instruction_preview_limit)
322
+ if trimmed:
323
+ payload["instruction"] = preview
324
+ return payload, trimmed
325
+
326
+
327
+ def _show_instruction_trim_hint(
328
+ ctx: Any,
329
+ *,
330
+ trimmed: bool,
331
+ preview_limit: int,
332
+ ) -> None:
333
+ """Render hint describing how to expand or collapse the instruction preview."""
334
+ if not trimmed or preview_limit <= 0:
335
+ return
336
+
337
+ view = get_ctx_value(ctx, "view", "rich") if ctx is not None else "rich"
338
+ if view != "rich": # pragma: no cover - non-rich view handling
339
+ return
340
+
341
+ suffix = f"[dim](preview: {preview_limit:,} chars)[/]"
342
+ if in_slash_mode(ctx):
343
+ console.print(
344
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Use '/details' again to toggle between trimmed and full prompts {suffix}"
345
+ )
346
+ return
347
+
348
+ console.print( # pragma: no cover - fallback hint rendering
349
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Run 'aip agents get <agent> --instruction-preview <n>' "
350
+ f"to control prompt preview length {suffix}"
351
+ )
352
+
353
+
354
+ def _display_agent_details(
355
+ ctx: Any,
356
+ client: Any,
357
+ agent: Any,
358
+ *,
359
+ instruction_preview_limit: int | None = None,
360
+ ) -> None:
361
+ """Display full agent details using raw API data to preserve ALL fields."""
362
+ if agent is None:
363
+ handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
364
+ return
365
+
366
+ preview_limit = _clamp_instruction_preview_limit(instruction_preview_limit)
367
+ trimmed_instruction = False
368
+
369
+ # Try to fetch and format raw agent data first
370
+ with spinner_context(
371
+ ctx,
372
+ "[bold blue]Loading agent details…[/bold blue]",
373
+ console_override=console,
374
+ ):
375
+ formatted_data = _fetch_and_format_raw_agent_data(client, agent)
376
+
377
+ if formatted_data:
378
+ # Use raw API data - this preserves ALL fields including account_id
379
+ panel_title = f"{ICON_AGENT} {formatted_data.get('name', 'Unknown')}"
380
+ payload, trimmed_instruction = _prepare_agent_details_payload(
381
+ formatted_data,
382
+ instruction_preview_limit=preview_limit,
383
+ )
384
+ output_result(
385
+ ctx,
386
+ payload,
387
+ title=panel_title,
388
+ )
389
+ else:
390
+ # Fall back to Pydantic model data if raw fetch fails
391
+ handle_rich_output(
392
+ ctx,
393
+ markup_text(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]"),
394
+ )
395
+
396
+ with spinner_context(
397
+ ctx,
398
+ "[bold blue]Preparing fallback agent details…[/bold blue]",
399
+ console_override=console,
400
+ ):
401
+ result_data = _format_fallback_agent_data(client, agent)
402
+
403
+ # Display using output_result
404
+ payload, trimmed_instruction = _prepare_agent_details_payload(
405
+ result_data,
406
+ instruction_preview_limit=preview_limit,
407
+ )
408
+ output_result(
409
+ ctx,
410
+ payload,
411
+ title="Agent Details",
412
+ )
413
+
414
+ _show_instruction_trim_hint(
415
+ ctx,
416
+ trimmed=trimmed_instruction,
417
+ preview_limit=preview_limit,
418
+ )
419
+
420
+
421
+ @click.group(name="agents", no_args_is_help=True)
422
+ def agents_group() -> None:
423
+ """Agent management operations."""
424
+ pass
425
+
426
+
427
+ def _resolve_agent(
428
+ ctx: Any,
429
+ client: Any,
430
+ ref: str,
431
+ select: int | None = None,
432
+ interface_preference: str = "fuzzy",
433
+ ) -> Any | None:
434
+ """Resolve an agent by ID or name, supporting fuzzy and questionary interfaces.
435
+
436
+ This function provides agent-specific resolution with flexible UI options.
437
+ It wraps resolve_resource_reference with agent-specific configuration, allowing
438
+ users to choose between fuzzy search and traditional questionary selection.
439
+
440
+ Args:
441
+ ctx: Click context for CLI command execution.
442
+ client: AIP SDK client instance.
443
+ ref: Agent identifier (UUID or name string).
444
+ select: Pre-selected index for non-interactive resolution (1-based).
445
+ interface_preference: UI preference - "fuzzy" for search or "questionary" for list.
446
+
447
+ Returns:
448
+ Agent object when found, None when resolution fails.
449
+ """
450
+ # Configure agent-specific resolution parameters
451
+ resolution_config = {
452
+ "resource_type": "agent",
453
+ "get_by_id": client.agents.get_agent_by_id,
454
+ "find_by_name": client.agents.find_agents,
455
+ "label": "Agent",
456
+ }
457
+ # Use agent-specific resolution with flexible interface preference
458
+ return resolve_resource_reference(
459
+ ctx,
460
+ client,
461
+ ref,
462
+ resolution_config["resource_type"],
463
+ resolution_config["get_by_id"],
464
+ resolution_config["find_by_name"],
465
+ resolution_config["label"],
466
+ select=select,
467
+ interface_preference=interface_preference,
468
+ )
469
+
470
+
471
+ def _get_agent_for_update(client: Any, agent_id: str) -> Any:
472
+ """Resolve an agent reference for update operations."""
473
+ try:
474
+ return client.agents.get_agent_by_id(agent_id)
475
+ except Exception:
476
+ # Fall back to name-based resolution below.
477
+ pass
478
+
479
+ try:
480
+ matches = client.agents.find_agents(name=agent_id)
481
+ except Exception as e:
482
+ raise click.ClickException(f"Agent not found: {agent_id} ({e})") from e
483
+
484
+ match_list: list[Any]
485
+ if matches is None:
486
+ match_list = []
487
+ elif isinstance(matches, list):
488
+ match_list = matches
489
+ else:
490
+ try:
491
+ match_list = list(matches)
492
+ except TypeError:
493
+ match_list = []
494
+
495
+ if not match_list:
496
+ raise click.ClickException(f"Agent not found: {agent_id}")
497
+ if len(match_list) > 1:
498
+ raise click.ClickException(f"Multiple agents named '{agent_id}'. Use ID instead.")
499
+ return match_list[0]
500
+
501
+
502
+ def _running_in_slash_mode(ctx: Any) -> bool:
503
+ """Return True if the command is executing inside the slash session."""
504
+ ctx_obj = getattr(ctx, "obj", None)
505
+ return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
506
+
507
+
508
+ def _emit_verbose_guidance(ctx: Any) -> None:
509
+ """Explain the modern alternative to the deprecated --verbose flag."""
510
+ if _running_in_slash_mode(ctx):
511
+ message = (
512
+ "[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
513
+ "the post-run viewer (Ctrl+T) to inspect the transcript."
514
+ )
515
+ else:
516
+ message = (
517
+ "[dim]Tip:[/] `--verbose` is no longer supported. Re-run without the flag and toggle the post-run viewer "
518
+ "(Ctrl+T) for detailed output."
519
+ )
520
+ handle_rich_output(ctx, markup_text(message))
521
+
522
+
523
+ def _get_language_model_display_name(agent: Any, model: str | None) -> str:
524
+ """Get display name for the language model."""
525
+ lm_display = getattr(agent, "model", None)
526
+ if not lm_display:
527
+ cfg = getattr(agent, "agent_config", {}) or {}
528
+ lm_display = cfg.get("lm_name") or cfg.get("model") or model or f"{DEFAULT_MODEL} (backend default)"
529
+ return lm_display
530
+
531
+
532
+ def _handle_command_exception(ctx: Any, e: Exception) -> None:
533
+ """Handle exceptions during command execution with consistent error handling."""
534
+ if isinstance(e, click.ClickException):
535
+ if get_ctx_value(ctx, "view") == "json":
536
+ handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
537
+ raise
538
+
539
+ handle_json_output(ctx, error=e)
540
+ if get_ctx_value(ctx, "view") != "json":
541
+ print_api_error(e)
542
+ raise click.exceptions.Exit(1) from e
543
+
544
+
545
+ def _handle_click_exception_for_json(ctx: Any, exc: click.ClickException) -> None:
546
+ """Handle ClickException with JSON output support, then re-raise.
547
+
548
+ This helper extracts the common pattern used in agent commands for handling
549
+ ClickExceptions with JSON output support.
550
+
551
+ Args:
552
+ ctx: Click context.
553
+ exc: The ClickException to handle.
554
+
555
+ Raises:
556
+ click.ClickException: Always re-raises the exception after handling JSON output.
557
+ """
558
+ # Handle JSON output for ClickExceptions if view is JSON
559
+ if get_ctx_value(ctx, "view") == "json":
560
+ handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
561
+ # Re-raise ClickExceptions without additional processing
562
+ raise exc