glaip-sdk 0.0.20__py3-none-any.whl → 0.6.5b6__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 (157) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1126 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +265 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +270 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +265 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +61 -28
  42. glaip_sdk/cli/slash/prompt.py +13 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +772 -222
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +77 -329
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1309
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +218 -78
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +161 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/icons.py +9 -3
  74. glaip_sdk/mcps/__init__.py +21 -0
  75. glaip_sdk/mcps/base.py +345 -0
  76. glaip_sdk/models/__init__.py +90 -0
  77. glaip_sdk/models/agent.py +47 -0
  78. glaip_sdk/models/agent_runs.py +116 -0
  79. glaip_sdk/models/common.py +42 -0
  80. glaip_sdk/models/mcp.py +33 -0
  81. glaip_sdk/models/tool.py +33 -0
  82. glaip_sdk/payload_schemas/__init__.py +1 -13
  83. glaip_sdk/payload_schemas/agent.py +1 -3
  84. glaip_sdk/registry/__init__.py +55 -0
  85. glaip_sdk/registry/agent.py +164 -0
  86. glaip_sdk/registry/base.py +139 -0
  87. glaip_sdk/registry/mcp.py +253 -0
  88. glaip_sdk/registry/tool.py +231 -0
  89. glaip_sdk/rich_components.py +58 -2
  90. glaip_sdk/runner/__init__.py +59 -0
  91. glaip_sdk/runner/base.py +84 -0
  92. glaip_sdk/runner/deps.py +115 -0
  93. glaip_sdk/runner/langgraph.py +597 -0
  94. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  95. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  96. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +158 -0
  97. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  98. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  99. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  100. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +177 -0
  101. glaip_sdk/tools/__init__.py +22 -0
  102. glaip_sdk/tools/base.py +435 -0
  103. glaip_sdk/utils/__init__.py +58 -12
  104. glaip_sdk/utils/a2a/__init__.py +34 -0
  105. glaip_sdk/utils/a2a/event_processor.py +188 -0
  106. glaip_sdk/utils/agent_config.py +4 -14
  107. glaip_sdk/utils/bundler.py +267 -0
  108. glaip_sdk/utils/client.py +111 -0
  109. glaip_sdk/utils/client_utils.py +46 -28
  110. glaip_sdk/utils/datetime_helpers.py +58 -0
  111. glaip_sdk/utils/discovery.py +78 -0
  112. glaip_sdk/utils/display.py +25 -21
  113. glaip_sdk/utils/export.py +143 -0
  114. glaip_sdk/utils/general.py +1 -36
  115. glaip_sdk/utils/import_export.py +15 -16
  116. glaip_sdk/utils/import_resolver.py +492 -0
  117. glaip_sdk/utils/instructions.py +101 -0
  118. glaip_sdk/utils/rendering/__init__.py +115 -1
  119. glaip_sdk/utils/rendering/formatting.py +38 -23
  120. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  121. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  122. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  123. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  124. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  125. glaip_sdk/utils/rendering/models.py +18 -8
  126. glaip_sdk/utils/rendering/renderer/__init__.py +9 -51
  127. glaip_sdk/utils/rendering/renderer/base.py +476 -882
  128. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  129. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  130. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  131. glaip_sdk/utils/rendering/renderer/stream.py +13 -54
  132. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  133. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  134. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  135. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  136. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  137. glaip_sdk/utils/rendering/state.py +204 -0
  138. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  139. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  140. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  141. glaip_sdk/utils/rendering/steps/format.py +176 -0
  142. glaip_sdk/utils/rendering/{steps.py → steps/manager.py} +122 -26
  143. glaip_sdk/utils/rendering/timing.py +36 -0
  144. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  145. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  146. glaip_sdk/utils/resource_refs.py +29 -26
  147. glaip_sdk/utils/runtime_config.py +422 -0
  148. glaip_sdk/utils/serialization.py +32 -46
  149. glaip_sdk/utils/sync.py +142 -0
  150. glaip_sdk/utils/tool_detection.py +33 -0
  151. glaip_sdk/utils/validation.py +20 -28
  152. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/METADATA +49 -4
  153. glaip_sdk-0.6.5b6.dist-info/RECORD +159 -0
  154. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/WHEEL +1 -1
  155. glaip_sdk/models.py +0 -259
  156. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  157. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.6.5b6.dist-info}/entry_points.txt +0 -0
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import json
10
10
  import os
11
11
  from collections.abc import Mapping
12
+ from copy import deepcopy
12
13
  from pathlib import Path
13
14
  from typing import Any
14
15
 
@@ -18,6 +19,7 @@ from rich.console import Console
18
19
  from glaip_sdk.branding import (
19
20
  ACCENT_STYLE,
20
21
  ERROR_STYLE,
22
+ HINT_PREFIX_STYLE,
21
23
  INFO,
22
24
  SUCCESS,
23
25
  SUCCESS_STYLE,
@@ -32,7 +34,8 @@ from glaip_sdk.cli.agent_config import (
32
34
  from glaip_sdk.cli.agent_config import (
33
35
  sanitize_agent_config_for_cli as sanitize_agent_config,
34
36
  )
35
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
37
+ from glaip_sdk.cli.constants import DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
38
+ from glaip_sdk.cli.context import get_ctx_value, output_flags
36
39
  from glaip_sdk.cli.display import (
37
40
  build_resource_result_data,
38
41
  display_agent_run_suggestions,
@@ -44,9 +47,7 @@ from glaip_sdk.cli.display import (
44
47
  handle_rich_output,
45
48
  print_api_error,
46
49
  )
47
- from glaip_sdk.cli.io import (
48
- export_resource_to_file_with_validation as export_resource_to_file,
49
- )
50
+ from glaip_sdk.cli.hints import in_slash_mode
50
51
  from glaip_sdk.cli.io import (
51
52
  fetch_raw_resource_details,
52
53
  )
@@ -64,9 +65,11 @@ from glaip_sdk.cli.utils import (
64
65
  build_renderer,
65
66
  coerce_to_row,
66
67
  get_client,
68
+ handle_resource_export,
67
69
  output_list,
68
70
  output_result,
69
71
  spinner_context,
72
+ with_client_and_spinner,
70
73
  )
71
74
  from glaip_sdk.cli.validators import (
72
75
  validate_agent_instruction_cli as validate_agent_instruction,
@@ -77,12 +80,13 @@ from glaip_sdk.cli.validators import (
77
80
  from glaip_sdk.cli.validators import (
78
81
  validate_timeout_cli as validate_timeout,
79
82
  )
80
- from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
83
+ from glaip_sdk.config.constants import AGENT_CONFIG_FIELDS, DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
81
84
  from glaip_sdk.exceptions import AgentTimeoutError
82
85
  from glaip_sdk.icons import ICON_AGENT
83
86
  from glaip_sdk.utils import format_datetime, is_uuid
84
87
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
85
88
  from glaip_sdk.utils.import_export import convert_export_to_import_format
89
+ from glaip_sdk.utils.rendering.renderer.toggle import TranscriptToggleController
86
90
  from glaip_sdk.utils.validation import coerce_timeout
87
91
 
88
92
  console = Console()
@@ -90,6 +94,8 @@ console = Console()
90
94
  # Error message constants
91
95
  AGENT_NOT_FOUND_ERROR = "Agent not found"
92
96
 
97
+ # Instruction preview controls
98
+
93
99
 
94
100
  def _safe_agent_attribute(agent: Any, name: str) -> Any:
95
101
  """Return attribute value for ``name`` while filtering Mock sentinels."""
@@ -142,10 +148,7 @@ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
142
148
  "description",
143
149
  "model",
144
150
  "agent_config",
145
- "tools",
146
- "agents",
147
- "mcps",
148
- "timeout",
151
+ *[field for field in AGENT_CONFIG_FIELDS if field not in ("name", "instruction", "model")],
149
152
  "tool_configs",
150
153
  )
151
154
 
@@ -246,9 +249,7 @@ def _resolve_resources_by_name(
246
249
  if not matches:
247
250
  raise click.ClickException(f"{label} not found: {ref}")
248
251
  if len(matches) > 1:
249
- raise click.ClickException(
250
- f"Multiple {resource_type}s named '{ref}'. Use ID instead."
251
- )
252
+ raise click.ClickException(f"Multiple {resource_type}s named '{ref}'. Use ID instead.")
252
253
  out.append(str(matches[0].id))
253
254
  return out
254
255
 
@@ -311,12 +312,92 @@ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
311
312
  return result_data
312
313
 
313
314
 
314
- def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
315
+ def _clamp_instruction_preview_limit(limit: int | None) -> int:
316
+ """Normalise preview limit; 0 disables trimming."""
317
+ default = DEFAULT_AGENT_INSTRUCTION_PREVIEW_LIMIT
318
+ if limit is None: # pragma: no cover
319
+ return default
320
+ try:
321
+ limit_value = int(limit)
322
+ except (TypeError, ValueError): # pragma: no cover - defensive parsing
323
+ return default
324
+
325
+ if limit_value <= 0:
326
+ return 0
327
+
328
+ return limit_value
329
+
330
+
331
+ def _build_instruction_preview(value: Any, limit: int) -> tuple[Any, bool]:
332
+ """Return a trimmed preview for long instruction strings."""
333
+ if not isinstance(value, str) or limit <= 0: # pragma: no cover
334
+ return value, False
335
+
336
+ if len(value) <= limit:
337
+ return value, False
338
+
339
+ trimmed_value = value[:limit].rstrip()
340
+ preview = f"{trimmed_value}\n\n... (preview trimmed)"
341
+ return preview, True
342
+
343
+
344
+ def _prepare_agent_details_payload(
345
+ data: dict[str, Any],
346
+ *,
347
+ instruction_preview_limit: int,
348
+ ) -> tuple[dict[str, Any], bool]:
349
+ """Return payload ready for rendering plus trim indicator."""
350
+ payload = deepcopy(data)
351
+ trimmed = False
352
+ if instruction_preview_limit > 0:
353
+ preview, trimmed = _build_instruction_preview(payload.get("instruction"), instruction_preview_limit)
354
+ if trimmed:
355
+ payload["instruction"] = preview
356
+ return payload, trimmed
357
+
358
+
359
+ def _show_instruction_trim_hint(
360
+ ctx: Any,
361
+ *,
362
+ trimmed: bool,
363
+ preview_limit: int,
364
+ ) -> None:
365
+ """Render hint describing how to expand or collapse the instruction preview."""
366
+ if not trimmed or preview_limit <= 0:
367
+ return
368
+
369
+ view = get_ctx_value(ctx, "view", "rich") if ctx is not None else "rich"
370
+ if view != "rich": # pragma: no cover - non-rich view handling
371
+ return
372
+
373
+ suffix = f"[dim](preview: {preview_limit:,} chars)[/]"
374
+ if in_slash_mode(ctx):
375
+ console.print(
376
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Use '/details' again to toggle between trimmed and full prompts {suffix}"
377
+ )
378
+ return
379
+
380
+ console.print( # pragma: no cover - fallback hint rendering
381
+ f"[{HINT_PREFIX_STYLE}]Tip:[/] Run 'aip agents get <agent> --instruction-preview <n>' "
382
+ f"to control prompt preview length {suffix}"
383
+ )
384
+
385
+
386
+ def _display_agent_details(
387
+ ctx: Any,
388
+ client: Any,
389
+ agent: Any,
390
+ *,
391
+ instruction_preview_limit: int | None = None,
392
+ ) -> None:
315
393
  """Display full agent details using raw API data to preserve ALL fields."""
316
394
  if agent is None:
317
395
  handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
318
396
  return
319
397
 
398
+ preview_limit = _clamp_instruction_preview_limit(instruction_preview_limit)
399
+ trimmed_instruction = False
400
+
320
401
  # Try to fetch and format raw agent data first
321
402
  with spinner_context(
322
403
  ctx,
@@ -328,9 +409,13 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
328
409
  if formatted_data:
329
410
  # Use raw API data - this preserves ALL fields including account_id
330
411
  panel_title = f"{ICON_AGENT} {formatted_data.get('name', 'Unknown')}"
412
+ payload, trimmed_instruction = _prepare_agent_details_payload(
413
+ formatted_data,
414
+ instruction_preview_limit=preview_limit,
415
+ )
331
416
  output_result(
332
417
  ctx,
333
- formatted_data,
418
+ payload,
334
419
  title=panel_title,
335
420
  )
336
421
  else:
@@ -348,12 +433,22 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
348
433
  result_data = _format_fallback_agent_data(client, agent)
349
434
 
350
435
  # Display using output_result
436
+ payload, trimmed_instruction = _prepare_agent_details_payload(
437
+ result_data,
438
+ instruction_preview_limit=preview_limit,
439
+ )
351
440
  output_result(
352
441
  ctx,
353
- result_data,
442
+ payload,
354
443
  title="Agent Details",
355
444
  )
356
445
 
446
+ _show_instruction_trim_hint(
447
+ ctx,
448
+ trimmed=trimmed_instruction,
449
+ preview_limit=preview_limit,
450
+ )
451
+
357
452
 
358
453
  @click.group(name="agents", no_args_is_help=True)
359
454
  def agents_group() -> None:
@@ -368,41 +463,47 @@ def _resolve_agent(
368
463
  select: int | None = None,
369
464
  interface_preference: str = "fuzzy",
370
465
  ) -> Any | None:
371
- """Resolve agent reference (ID or name) with ambiguity handling.
466
+ """Resolve an agent by ID or name, supporting fuzzy and questionary interfaces.
467
+
468
+ This function provides agent-specific resolution with flexible UI options.
469
+ It wraps resolve_resource_reference with agent-specific configuration, allowing
470
+ users to choose between fuzzy search and traditional questionary selection.
372
471
 
373
472
  Args:
374
- ctx: Click context object for CLI operations.
375
- client: AIP client instance for API operations.
376
- ref: Agent reference (ID or name) to resolve.
377
- select: Pre-selected agent index for non-interactive mode.
378
- interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list.
473
+ ctx: Click context for CLI command execution.
474
+ client: AIP SDK client instance.
475
+ ref: Agent identifier (UUID or name string).
476
+ select: Pre-selected index for non-interactive resolution (1-based).
477
+ interface_preference: UI preference - "fuzzy" for search or "questionary" for list.
379
478
 
380
479
  Returns:
381
- Resolved agent object or None if not found.
480
+ Agent object when found, None when resolution fails.
382
481
  """
482
+ # Configure agent-specific resolution parameters
483
+ resolution_config = {
484
+ "resource_type": "agent",
485
+ "get_by_id": client.agents.get_agent_by_id,
486
+ "find_by_name": client.agents.find_agents,
487
+ "label": "Agent",
488
+ }
489
+ # Use agent-specific resolution with flexible interface preference
383
490
  return resolve_resource_reference(
384
491
  ctx,
385
492
  client,
386
493
  ref,
387
- "agent",
388
- client.agents.get_agent_by_id,
389
- client.agents.find_agents,
390
- "Agent",
494
+ resolution_config["resource_type"],
495
+ resolution_config["get_by_id"],
496
+ resolution_config["find_by_name"],
497
+ resolution_config["label"],
391
498
  select=select,
392
499
  interface_preference=interface_preference,
393
500
  )
394
501
 
395
502
 
396
503
  @agents_group.command(name="list")
397
- @click.option(
398
- "--simple", is_flag=True, help="Show simple table without interactive picker"
399
- )
400
- @click.option(
401
- "--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)"
402
- )
403
- @click.option(
404
- "--framework", help="Filter by framework (langchain, langgraph, google_adk)"
405
- )
504
+ @click.option("--simple", is_flag=True, help="Show simple table without interactive picker")
505
+ @click.option("--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)")
506
+ @click.option("--framework", help="Filter by framework (langchain, langgraph, google_adk)")
406
507
  @click.option("--name", help="Filter by partial name match (case-insensitive)")
407
508
  @click.option("--version", help="Filter by exact version match")
408
509
  @click.option(
@@ -423,19 +524,20 @@ def list_agents(
423
524
  ) -> None:
424
525
  """List agents with optional filtering."""
425
526
  try:
426
- client = get_client(ctx)
427
- with spinner_context(
527
+ with with_client_and_spinner(
428
528
  ctx,
429
529
  "[bold blue]Fetching agents…[/bold blue]",
430
530
  console_override=console,
431
- ):
432
- agents = client.agents.list_agents(
433
- agent_type=agent_type,
434
- framework=framework,
435
- name=name,
436
- version=version,
437
- sync_langflow_agents=sync_langflow,
438
- )
531
+ ) as client:
532
+ # Query agents with specified filters
533
+ filter_params = {
534
+ "agent_type": agent_type,
535
+ "framework": framework,
536
+ "name": name,
537
+ "version": version,
538
+ "sync_langflow_agents": sync_langflow,
539
+ }
540
+ agents = client.agents.list_agents(**filter_params)
439
541
 
440
542
  # Define table columns: (data_key, header, style, width)
441
543
  columns = [
@@ -448,6 +550,14 @@ def list_agents(
448
550
 
449
551
  # Transform function for safe attribute access
450
552
  def transform_agent(agent: Any) -> dict[str, Any]:
553
+ """Transform an agent object to a display row dictionary.
554
+
555
+ Args:
556
+ agent: Agent object to transform.
557
+
558
+ Returns:
559
+ Dictionary with id, name, type, framework, and version fields.
560
+ """
451
561
  row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
452
562
  # Ensure id is always a string
453
563
  row["id"] = str(row["id"])
@@ -466,7 +576,10 @@ def list_agents(
466
576
  and len(agents) > 0
467
577
  )
468
578
 
579
+ # Track picker attempt so the fallback table doesn't re-open the palette
580
+ picker_attempted = False
469
581
  if interactive_enabled:
582
+ picker_attempted = True
470
583
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
471
584
  if picked_agent:
472
585
  _display_agent_details(ctx, client, picked_agent)
@@ -481,15 +594,17 @@ def list_agents(
481
594
  f"{ICON_AGENT} Available Agents",
482
595
  columns,
483
596
  transform_agent,
484
- skip_picker=simple
485
- or any(
486
- param is not None for param in (agent_type, framework, name, version)
597
+ skip_picker=(
598
+ not interactive_enabled
599
+ or picker_attempted
600
+ or simple
601
+ or any(param is not None for param in (agent_type, framework, name, version))
487
602
  ),
488
603
  use_pager=False,
489
604
  )
490
605
 
491
606
  except Exception as e:
492
- raise click.ClickException(str(e))
607
+ raise click.ClickException(str(e)) from e
493
608
 
494
609
 
495
610
  @agents_group.command()
@@ -500,68 +615,66 @@ def list_agents(
500
615
  type=click.Path(dir_okay=False, writable=True),
501
616
  help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
502
617
  )
618
+ @click.option(
619
+ "--instruction-preview",
620
+ type=int,
621
+ default=0,
622
+ show_default=True,
623
+ help="Instruction preview length when printing instructions (0 shows full prompt).",
624
+ )
503
625
  @output_flags()
504
626
  @click.pass_context
505
- def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> None:
506
- """Get agent details.
627
+ def get(
628
+ ctx: Any,
629
+ agent_ref: str,
630
+ select: int | None,
631
+ export: str | None,
632
+ instruction_preview: int,
633
+ ) -> None:
634
+ r"""Get agent details.
507
635
 
636
+ \b
508
637
  Examples:
509
638
  aip agents get my-agent
510
639
  aip agents get my-agent --export agent.json # Exports complete configuration as JSON
511
640
  aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
512
641
  """
513
642
  try:
514
- client = get_client(ctx)
643
+ # Initialize API client for agent retrieval
644
+ api_client = get_client(ctx)
515
645
 
516
- # Resolve agent with ambiguity handling - use questionary interface for traditional UX
517
- agent = _resolve_agent(
518
- ctx, client, agent_ref, select, interface_preference="questionary"
519
- )
646
+ # Resolve agent reference using questionary interface for better UX
647
+ agent = _resolve_agent(ctx, api_client, agent_ref, select, interface_preference="questionary")
520
648
 
521
- # Handle export option
649
+ if not agent:
650
+ raise click.ClickException(f"Agent '{agent_ref}' not found")
651
+
652
+ # Handle export option if requested
522
653
  if export:
523
- export_path = Path(export)
524
- # Auto-detect format from file extension
525
- detected_format = detect_export_format(export_path)
526
-
527
- # Always export comprehensive data - re-fetch agent with full details
528
- try:
529
- with spinner_context(
530
- ctx,
531
- "[bold blue]Fetching complete agent data…[/bold blue]",
532
- console_override=console,
533
- ):
534
- agent = client.agents.get_agent_by_id(agent.id)
535
- except Exception as e:
536
- handle_rich_output(
537
- ctx,
538
- markup_text(
539
- f"[{WARNING_STYLE}]⚠️ Could not fetch full agent details: {e}[/]"
540
- ),
541
- )
542
- handle_rich_output(
543
- ctx,
544
- markup_text(
545
- f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]"
546
- ),
547
- )
548
-
549
- export_resource_to_file(agent, export_path, detected_format)
550
- handle_rich_output(
654
+ handle_resource_export(
551
655
  ctx,
552
- markup_text(
553
- f"[{SUCCESS_STYLE}]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/]"
554
- ),
656
+ agent,
657
+ Path(export),
658
+ resource_type="agent",
659
+ get_by_id_func=api_client.agents.get_agent_by_id,
660
+ console_override=console,
555
661
  )
556
662
 
557
663
  # Display full agent details using the standardized helper
558
- _display_agent_details(ctx, client, agent)
664
+ _display_agent_details(
665
+ ctx,
666
+ api_client,
667
+ agent,
668
+ instruction_preview_limit=instruction_preview,
669
+ )
559
670
 
560
671
  # Show run suggestions via centralized display helper
561
672
  handle_rich_output(ctx, display_agent_run_suggestions(agent))
562
673
 
674
+ except click.ClickException:
675
+ raise
563
676
  except Exception as e:
564
- raise click.ClickException(str(e))
677
+ raise click.ClickException(str(e)) from e
565
678
 
566
679
 
567
680
  def _validate_run_input(input_option: str | None, input_text: str | None) -> str:
@@ -569,9 +682,7 @@ def _validate_run_input(input_option: str | None, input_text: str | None) -> str
569
682
  final_input_text = input_option if input_option else input_text
570
683
 
571
684
  if not final_input_text:
572
- raise click.ClickException(
573
- "Input text is required. Use either positional argument or --input option."
574
- )
685
+ raise click.ClickException("Input text is required. Use either positional argument or --input option.")
575
686
 
576
687
  return final_input_text
577
688
 
@@ -583,8 +694,8 @@ def _parse_chat_history(chat_history: str | None) -> list[dict[str, Any]] | None
583
694
 
584
695
  try:
585
696
  return json.loads(chat_history)
586
- except json.JSONDecodeError:
587
- raise click.ClickException("Invalid JSON in chat history")
697
+ except json.JSONDecodeError as err:
698
+ raise click.ClickException("Invalid JSON in chat history") from err
588
699
 
589
700
 
590
701
  def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
@@ -598,6 +709,23 @@ def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
598
709
  )
599
710
 
600
711
 
712
+ def _maybe_attach_transcript_toggle(ctx: Any, renderer: Any) -> None:
713
+ """Attach transcript toggle controller when interactive TTY is available."""
714
+ if renderer is None:
715
+ return
716
+
717
+ console_obj = getattr(renderer, "console", None)
718
+ if console_obj is None or not getattr(console_obj, "is_terminal", False):
719
+ return
720
+
721
+ tty_enabled = bool(get_ctx_value(ctx, "tty", True))
722
+ if not tty_enabled:
723
+ return
724
+
725
+ controller = TranscriptToggleController(enabled=True)
726
+ renderer.transcript_controller = controller
727
+
728
+
601
729
  def _prepare_run_kwargs(
602
730
  agent: Any,
603
731
  final_input_text: str,
@@ -647,9 +775,7 @@ def _save_run_transcript(save: str | None, result: Any, working_console: Any) ->
647
775
  if ext == "json":
648
776
  save_data = {
649
777
  "output": result or "",
650
- "full_debug_output": getattr(
651
- working_console, "get_captured_output", lambda: ""
652
- )(),
778
+ "full_debug_output": getattr(working_console, "get_captured_output", lambda: "")(),
653
779
  "timestamp": "captured during agent execution",
654
780
  }
655
781
  content = json.dumps(save_data, indent=2)
@@ -662,9 +788,7 @@ def _save_run_transcript(save: str | None, result: Any, working_console: Any) ->
662
788
 
663
789
  with open(save, "w", encoding="utf-8") as f:
664
790
  f.write(content)
665
- print_markup(
666
- f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console
667
- )
791
+ print_markup(f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console)
668
792
 
669
793
 
670
794
  @agents_group.command()
@@ -710,10 +834,11 @@ def run(
710
834
  files: tuple[str, ...] | None,
711
835
  verbose: bool,
712
836
  ) -> None:
713
- """Run an agent with input text.
837
+ r"""Run an agent with input text.
714
838
 
715
839
  Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
716
840
 
841
+ \b
717
842
  Examples:
718
843
  aip agents run my-agent "Hello world"
719
844
  aip agents run agent-123 "Process this data" --timeout 600
@@ -727,12 +852,11 @@ def run(
727
852
 
728
853
  try:
729
854
  client = get_client(ctx)
730
- agent = _resolve_agent(
731
- ctx, client, agent_ref, select, interface_preference="fuzzy"
732
- )
855
+ agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="fuzzy")
733
856
 
734
857
  parsed_chat_history = _parse_chat_history(chat_history)
735
858
  renderer, working_console = _setup_run_renderer(ctx, save, verbose)
859
+ _maybe_attach_transcript_toggle(ctx, renderer)
736
860
 
737
861
  try:
738
862
  client.timeout = float(timeout)
@@ -777,17 +901,19 @@ def run(
777
901
  except AgentTimeoutError as e:
778
902
  error_msg = str(e)
779
903
  handle_json_output(ctx, error=Exception(error_msg))
780
- raise click.ClickException(error_msg)
904
+ raise click.ClickException(error_msg) from e
781
905
  except Exception as e:
782
906
  _handle_command_exception(ctx, e)
783
907
 
784
908
 
785
909
  def _running_in_slash_mode(ctx: Any) -> bool:
910
+ """Return True if the command is executing inside the slash session."""
786
911
  ctx_obj = getattr(ctx, "obj", None)
787
912
  return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
788
913
 
789
914
 
790
915
  def _emit_verbose_guidance(ctx: Any) -> None:
916
+ """Explain the modern alternative to the deprecated --verbose flag."""
791
917
  if _running_in_slash_mode(ctx):
792
918
  message = (
793
919
  "[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
@@ -866,16 +992,12 @@ def _extract_and_validate_fields(
866
992
  if not name:
867
993
  raise click.ClickException("Agent name is required (--name or --import)")
868
994
  if not instruction:
869
- raise click.ClickException(
870
- "Agent instruction is required (--instruction or --import)"
871
- )
995
+ raise click.ClickException("Agent instruction is required (--instruction or --import)")
872
996
 
873
997
  return name, instruction, model, tools, agents, mcps, timeout
874
998
 
875
999
 
876
- def _validate_and_coerce_fields(
877
- name: str, instruction: str, timeout: Any
878
- ) -> tuple[str, str, Any]:
1000
+ def _validate_and_coerce_fields(name: str, instruction: str, timeout: Any) -> tuple[str, str, Any]:
879
1001
  """Validate and coerce field values."""
880
1002
  name = validate_agent_name(name)
881
1003
  instruction = validate_agent_instruction(instruction)
@@ -886,19 +1008,11 @@ def _validate_and_coerce_fields(
886
1008
  return name, instruction, timeout
887
1009
 
888
1010
 
889
- def _resolve_resources(
890
- client: Any, tools: tuple, agents: tuple, mcps: tuple
891
- ) -> tuple[list, list, list]:
1011
+ def _resolve_resources(client: Any, tools: tuple, agents: tuple, mcps: tuple) -> tuple[list, list, list]:
892
1012
  """Resolve tool, agent, and MCP references."""
893
- resolved_tools = _resolve_resources_by_name(
894
- client, tools, "tool", client.find_tools, "Tool"
895
- )
896
- resolved_agents = _resolve_resources_by_name(
897
- client, agents, "agent", client.find_agents, "Agent"
898
- )
899
- resolved_mcps = _resolve_resources_by_name(
900
- client, mcps, "mcp", client.find_mcps, "MCP"
901
- )
1013
+ resolved_tools = _resolve_resources_by_name(client, tools, "tool", client.find_tools, "Tool")
1014
+ resolved_agents = _resolve_resources_by_name(client, agents, "agent", client.find_agents, "Agent")
1015
+ resolved_mcps = _resolve_resources_by_name(client, mcps, "mcp", client.find_mcps, "MCP")
902
1016
 
903
1017
  return resolved_tools, resolved_agents, resolved_mcps
904
1018
 
@@ -925,16 +1039,12 @@ def _build_create_kwargs(
925
1039
  }
926
1040
 
927
1041
  # Handle language model selection
928
- lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(
929
- merged_data, model
930
- )
1042
+ lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(merged_data, model)
931
1043
  create_kwargs.update(lm_selection_dict)
932
1044
 
933
1045
  # Handle import file specific logic
934
1046
  if import_file:
935
- _add_import_file_attributes(
936
- create_kwargs, merged_data, should_strip_lm_identity
937
- )
1047
+ _add_import_file_attributes(create_kwargs, merged_data, should_strip_lm_identity)
938
1048
 
939
1049
  return create_kwargs
940
1050
 
@@ -980,12 +1090,7 @@ def _get_language_model_display_name(agent: Any, model: str | None) -> str:
980
1090
  lm_display = getattr(agent, "model", None)
981
1091
  if not lm_display:
982
1092
  cfg = getattr(agent, "agent_config", {}) or {}
983
- lm_display = (
984
- cfg.get("lm_name")
985
- or cfg.get("model")
986
- or model
987
- or f"{DEFAULT_MODEL} (backend default)"
988
- )
1093
+ lm_display = cfg.get("lm_name") or cfg.get("model") or model or f"{DEFAULT_MODEL} (backend default)"
989
1094
  return lm_display
990
1095
 
991
1096
 
@@ -1020,7 +1125,7 @@ def _handle_command_exception(ctx: Any, e: Exception) -> None:
1020
1125
  handle_json_output(ctx, error=e)
1021
1126
  if get_ctx_value(ctx, "view") != "json":
1022
1127
  print_api_error(e)
1023
- raise click.ClickException(str(e))
1128
+ raise click.exceptions.Exit(1) from e
1024
1129
 
1025
1130
 
1026
1131
  def _handle_creation_exception(ctx: Any, e: Exception) -> None:
@@ -1063,8 +1168,9 @@ def create(
1063
1168
  timeout: float | None,
1064
1169
  import_file: str | None,
1065
1170
  ) -> None:
1066
- """Create a new agent.
1171
+ r"""Create a new agent.
1067
1172
 
1173
+ \b
1068
1174
  Examples:
1069
1175
  aip agents create --name "My Agent" --instruction "You are a helpful assistant"
1070
1176
  aip agents create --import agent.json
@@ -1074,13 +1180,9 @@ def create(
1074
1180
 
1075
1181
  # Handle import file or CLI args
1076
1182
  if import_file:
1077
- merged_data = _handle_import_file_logic(
1078
- import_file, model, name, instruction, tools, agents, mcps, timeout
1079
- )
1183
+ merged_data = _handle_import_file_logic(import_file, model, name, instruction, tools, agents, mcps, timeout)
1080
1184
  else:
1081
- merged_data = _build_cli_args_data(
1082
- name, instruction, model, tools, agents, mcps, timeout
1083
- )
1185
+ merged_data = _build_cli_args_data(name, instruction, model, tools, agents, mcps, timeout)
1084
1186
 
1085
1187
  # Extract and validate fields
1086
1188
  (
@@ -1092,14 +1194,10 @@ def create(
1092
1194
  mcps,
1093
1195
  timeout,
1094
1196
  ) = _extract_and_validate_fields(merged_data)
1095
- name, instruction, timeout = _validate_and_coerce_fields(
1096
- name, instruction, timeout
1097
- )
1197
+ name, instruction, timeout = _validate_and_coerce_fields(name, instruction, timeout)
1098
1198
 
1099
1199
  # Resolve resources
1100
- resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(
1101
- client, tools, agents, mcps
1102
- )
1200
+ resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(client, tools, agents, mcps)
1103
1201
 
1104
1202
  # Build create kwargs
1105
1203
  create_kwargs = _build_create_kwargs(
@@ -1129,7 +1227,7 @@ def _get_agent_for_update(client: Any, agent_id: str) -> Any:
1129
1227
  try:
1130
1228
  return client.agents.get_agent_by_id(agent_id)
1131
1229
  except Exception as e:
1132
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
1230
+ raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
1133
1231
 
1134
1232
 
1135
1233
  def _handle_update_import_file(
@@ -1211,16 +1309,12 @@ def _handle_update_import_config(
1211
1309
  if not import_file:
1212
1310
  return
1213
1311
 
1214
- lm_selection, should_strip_lm_identity = resolve_language_model_selection(
1215
- merged_data, None
1216
- )
1312
+ lm_selection, should_strip_lm_identity = resolve_language_model_selection(merged_data, None)
1217
1313
  update_data.update(lm_selection)
1218
1314
 
1219
1315
  raw_cfg = merged_data.get("agent_config") if isinstance(merged_data, dict) else None
1220
1316
  if isinstance(raw_cfg, dict):
1221
- update_data["agent_config"] = sanitize_agent_config(
1222
- raw_cfg, strip_lm_identity=should_strip_lm_identity
1223
- )
1317
+ update_data["agent_config"] = sanitize_agent_config(raw_cfg, strip_lm_identity=should_strip_lm_identity)
1224
1318
 
1225
1319
  excluded_fields = {
1226
1320
  "name",
@@ -1270,8 +1364,9 @@ def update(
1270
1364
  timeout: float | None,
1271
1365
  import_file: str | None,
1272
1366
  ) -> None:
1273
- """Update an existing agent.
1367
+ r"""Update an existing agent.
1274
1368
 
1369
+ \b
1275
1370
  Examples:
1276
1371
  aip agents update my-agent --instruction "New instruction"
1277
1372
  aip agents update my-agent --import agent.json
@@ -1289,16 +1384,18 @@ def update(
1289
1384
  agents,
1290
1385
  mcps,
1291
1386
  timeout,
1292
- ) = _handle_update_import_file(
1293
- import_file, name, instruction, tools, agents, mcps, timeout
1294
- )
1387
+ ) = _handle_update_import_file(import_file, name, instruction, tools, agents, mcps, timeout)
1295
1388
 
1296
- update_data = _build_update_data(
1297
- name, instruction, tools, agents, mcps, timeout
1298
- )
1389
+ update_data = _build_update_data(name, instruction, tools, agents, mcps, timeout)
1299
1390
 
1300
1391
  if merged_data:
1301
1392
  _handle_update_import_config(import_file, merged_data, update_data)
1393
+ # Ensure instruction from import file is included if not already set via CLI
1394
+ # This handles the case where instruction is None in CLI args but exists in import file
1395
+ if import_file and (instruction is None or "instruction" not in update_data):
1396
+ import_instruction = merged_data.get("instruction")
1397
+ if import_instruction is not None:
1398
+ update_data["instruction"] = import_instruction
1302
1399
 
1303
1400
  if not update_data:
1304
1401
  raise click.ClickException("No update fields specified")
@@ -1333,7 +1430,7 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1333
1430
  try:
1334
1431
  agent = client.agents.get_agent_by_id(agent_id)
1335
1432
  except Exception as e:
1336
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
1433
+ raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}") from e
1337
1434
 
1338
1435
  # Confirm deletion when not forced
1339
1436
  if not yes and not display_confirmation_prompt("Agent", agent.name):
@@ -1365,13 +1462,11 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1365
1462
  "--base-url",
1366
1463
  help="Custom LangFlow server base URL (overrides LANGFLOW_BASE_URL env var)",
1367
1464
  )
1368
- @click.option(
1369
- "--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)"
1370
- )
1465
+ @click.option("--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)")
1371
1466
  @output_flags()
1372
1467
  @click.pass_context
1373
1468
  def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1374
- """Sync agents with LangFlow server flows.
1469
+ r"""Sync agents with LangFlow server flows.
1375
1470
 
1376
1471
  This command fetches all flows from the configured LangFlow server and
1377
1472
  creates/updates corresponding agents in the platform.
@@ -1380,6 +1475,7 @@ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1380
1475
  - Command options (--base-url, --api-key)
1381
1476
  - Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
1382
1477
 
1478
+ \b
1383
1479
  Examples:
1384
1480
  aip agents sync-langflow
1385
1481
  aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
@@ -1396,15 +1492,16 @@ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1396
1492
  # Show success message for non-JSON output
1397
1493
  if get_ctx_value(ctx, "view") != "json":
1398
1494
  # Extract some useful info from the result
1399
- success_count = result.get("data", {}).get("created_count", 0) + result.get(
1400
- "data", {}
1401
- ).get("updated_count", 0)
1495
+ success_count = result.get("data", {}).get("created_count", 0) + result.get("data", {}).get(
1496
+ "updated_count", 0
1497
+ )
1402
1498
  total_count = result.get("data", {}).get("total_processed", 0)
1403
1499
 
1404
1500
  handle_rich_output(
1405
1501
  ctx,
1406
1502
  markup_text(
1407
- f"[{SUCCESS_STYLE}]✅ Successfully synced {success_count} LangFlow agents ({total_count} total processed)[/]"
1503
+ f"[{SUCCESS_STYLE}]✅ Successfully synced {success_count} LangFlow agents "
1504
+ f"({total_count} total processed)[/]"
1408
1505
  ),
1409
1506
  )
1410
1507