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