glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 (156) 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 +1191 -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 +251 -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 +266 -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 +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  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 +876 -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 +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  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 +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -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/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.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,7 +80,7 @@ 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
@@ -91,6 +94,8 @@ console = Console()
91
94
  # Error message constants
92
95
  AGENT_NOT_FOUND_ERROR = "Agent not found"
93
96
 
97
+ # Instruction preview controls
98
+
94
99
 
95
100
  def _safe_agent_attribute(agent: Any, name: str) -> Any:
96
101
  """Return attribute value for ``name`` while filtering Mock sentinels."""
@@ -143,10 +148,7 @@ def _build_fallback_agent_mapping(agent: Any) -> dict[str, Any]:
143
148
  "description",
144
149
  "model",
145
150
  "agent_config",
146
- "tools",
147
- "agents",
148
- "mcps",
149
- "timeout",
151
+ *[field for field in AGENT_CONFIG_FIELDS if field not in ("name", "instruction", "model")],
150
152
  "tool_configs",
151
153
  )
152
154
 
@@ -247,9 +249,7 @@ def _resolve_resources_by_name(
247
249
  if not matches:
248
250
  raise click.ClickException(f"{label} not found: {ref}")
249
251
  if len(matches) > 1:
250
- raise click.ClickException(
251
- f"Multiple {resource_type}s named '{ref}'. Use ID instead."
252
- )
252
+ raise click.ClickException(f"Multiple {resource_type}s named '{ref}'. Use ID instead.")
253
253
  out.append(str(matches[0].id))
254
254
  return out
255
255
 
@@ -312,12 +312,92 @@ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
312
312
  return result_data
313
313
 
314
314
 
315
- 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:
316
393
  """Display full agent details using raw API data to preserve ALL fields."""
317
394
  if agent is None:
318
395
  handle_rich_output(ctx, markup_text(f"[{ERROR_STYLE}]❌ No agent provided[/]"))
319
396
  return
320
397
 
398
+ preview_limit = _clamp_instruction_preview_limit(instruction_preview_limit)
399
+ trimmed_instruction = False
400
+
321
401
  # Try to fetch and format raw agent data first
322
402
  with spinner_context(
323
403
  ctx,
@@ -329,9 +409,13 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
329
409
  if formatted_data:
330
410
  # Use raw API data - this preserves ALL fields including account_id
331
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
+ )
332
416
  output_result(
333
417
  ctx,
334
- formatted_data,
418
+ payload,
335
419
  title=panel_title,
336
420
  )
337
421
  else:
@@ -349,12 +433,22 @@ def _display_agent_details(ctx: Any, client: Any, agent: Any) -> None:
349
433
  result_data = _format_fallback_agent_data(client, agent)
350
434
 
351
435
  # Display using output_result
436
+ payload, trimmed_instruction = _prepare_agent_details_payload(
437
+ result_data,
438
+ instruction_preview_limit=preview_limit,
439
+ )
352
440
  output_result(
353
441
  ctx,
354
- result_data,
442
+ payload,
355
443
  title="Agent Details",
356
444
  )
357
445
 
446
+ _show_instruction_trim_hint(
447
+ ctx,
448
+ trimmed=trimmed_instruction,
449
+ preview_limit=preview_limit,
450
+ )
451
+
358
452
 
359
453
  @click.group(name="agents", no_args_is_help=True)
360
454
  def agents_group() -> None:
@@ -369,41 +463,47 @@ def _resolve_agent(
369
463
  select: int | None = None,
370
464
  interface_preference: str = "fuzzy",
371
465
  ) -> Any | None:
372
- """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.
373
471
 
374
472
  Args:
375
- ctx: Click context object for CLI operations.
376
- client: AIP client instance for API operations.
377
- ref: Agent reference (ID or name) to resolve.
378
- select: Pre-selected agent index for non-interactive mode.
379
- 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.
380
478
 
381
479
  Returns:
382
- Resolved agent object or None if not found.
480
+ Agent object when found, None when resolution fails.
383
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
384
490
  return resolve_resource_reference(
385
491
  ctx,
386
492
  client,
387
493
  ref,
388
- "agent",
389
- client.agents.get_agent_by_id,
390
- client.agents.find_agents,
391
- "Agent",
494
+ resolution_config["resource_type"],
495
+ resolution_config["get_by_id"],
496
+ resolution_config["find_by_name"],
497
+ resolution_config["label"],
392
498
  select=select,
393
499
  interface_preference=interface_preference,
394
500
  )
395
501
 
396
502
 
397
503
  @agents_group.command(name="list")
398
- @click.option(
399
- "--simple", is_flag=True, help="Show simple table without interactive picker"
400
- )
401
- @click.option(
402
- "--type", "agent_type", help="Filter by agent type (config, code, a2a, langflow)"
403
- )
404
- @click.option(
405
- "--framework", help="Filter by framework (langchain, langgraph, google_adk)"
406
- )
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)")
407
507
  @click.option("--name", help="Filter by partial name match (case-insensitive)")
408
508
  @click.option("--version", help="Filter by exact version match")
409
509
  @click.option(
@@ -424,19 +524,20 @@ def list_agents(
424
524
  ) -> None:
425
525
  """List agents with optional filtering."""
426
526
  try:
427
- client = get_client(ctx)
428
- with spinner_context(
527
+ with with_client_and_spinner(
429
528
  ctx,
430
529
  "[bold blue]Fetching agents…[/bold blue]",
431
530
  console_override=console,
432
- ):
433
- agents = client.agents.list_agents(
434
- agent_type=agent_type,
435
- framework=framework,
436
- name=name,
437
- version=version,
438
- sync_langflow_agents=sync_langflow,
439
- )
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)
440
541
 
441
542
  # Define table columns: (data_key, header, style, width)
442
543
  columns = [
@@ -449,6 +550,14 @@ def list_agents(
449
550
 
450
551
  # Transform function for safe attribute access
451
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
+ """
452
561
  row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
453
562
  # Ensure id is always a string
454
563
  row["id"] = str(row["id"])
@@ -467,7 +576,10 @@ def list_agents(
467
576
  and len(agents) > 0
468
577
  )
469
578
 
579
+ # Track picker attempt so the fallback table doesn't re-open the palette
580
+ picker_attempted = False
470
581
  if interactive_enabled:
582
+ picker_attempted = True
471
583
  picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
472
584
  if picked_agent:
473
585
  _display_agent_details(ctx, client, picked_agent)
@@ -482,15 +594,17 @@ def list_agents(
482
594
  f"{ICON_AGENT} Available Agents",
483
595
  columns,
484
596
  transform_agent,
485
- skip_picker=simple
486
- or any(
487
- 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))
488
602
  ),
489
603
  use_pager=False,
490
604
  )
491
605
 
492
606
  except Exception as e:
493
- raise click.ClickException(str(e))
607
+ raise click.ClickException(str(e)) from e
494
608
 
495
609
 
496
610
  @agents_group.command()
@@ -501,68 +615,66 @@ def list_agents(
501
615
  type=click.Path(dir_okay=False, writable=True),
502
616
  help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
503
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
+ )
504
625
  @output_flags()
505
626
  @click.pass_context
506
- def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> None:
507
- """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.
508
635
 
636
+ \b
509
637
  Examples:
510
638
  aip agents get my-agent
511
639
  aip agents get my-agent --export agent.json # Exports complete configuration as JSON
512
640
  aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
513
641
  """
514
642
  try:
515
- client = get_client(ctx)
643
+ # Initialize API client for agent retrieval
644
+ api_client = get_client(ctx)
516
645
 
517
- # Resolve agent with ambiguity handling - use questionary interface for traditional UX
518
- agent = _resolve_agent(
519
- ctx, client, agent_ref, select, interface_preference="questionary"
520
- )
646
+ # Resolve agent reference using questionary interface for better UX
647
+ agent = _resolve_agent(ctx, api_client, agent_ref, select, interface_preference="questionary")
521
648
 
522
- # Handle export option
649
+ if not agent:
650
+ raise click.ClickException(f"Agent '{agent_ref}' not found")
651
+
652
+ # Handle export option if requested
523
653
  if export:
524
- export_path = Path(export)
525
- # Auto-detect format from file extension
526
- detected_format = detect_export_format(export_path)
527
-
528
- # Always export comprehensive data - re-fetch agent with full details
529
- try:
530
- with spinner_context(
531
- ctx,
532
- "[bold blue]Fetching complete agent data…[/bold blue]",
533
- console_override=console,
534
- ):
535
- agent = client.agents.get_agent_by_id(agent.id)
536
- except Exception as e:
537
- handle_rich_output(
538
- ctx,
539
- markup_text(
540
- f"[{WARNING_STYLE}]⚠️ Could not fetch full agent details: {e}[/]"
541
- ),
542
- )
543
- handle_rich_output(
544
- ctx,
545
- markup_text(
546
- f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]"
547
- ),
548
- )
549
-
550
- export_resource_to_file(agent, export_path, detected_format)
551
- handle_rich_output(
654
+ handle_resource_export(
552
655
  ctx,
553
- markup_text(
554
- f"[{SUCCESS_STYLE}]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/]"
555
- ),
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,
556
661
  )
557
662
 
558
663
  # Display full agent details using the standardized helper
559
- _display_agent_details(ctx, client, agent)
664
+ _display_agent_details(
665
+ ctx,
666
+ api_client,
667
+ agent,
668
+ instruction_preview_limit=instruction_preview,
669
+ )
560
670
 
561
671
  # Show run suggestions via centralized display helper
562
672
  handle_rich_output(ctx, display_agent_run_suggestions(agent))
563
673
 
674
+ except click.ClickException:
675
+ raise
564
676
  except Exception as e:
565
- raise click.ClickException(str(e))
677
+ raise click.ClickException(str(e)) from e
566
678
 
567
679
 
568
680
  def _validate_run_input(input_option: str | None, input_text: str | None) -> str:
@@ -570,9 +682,7 @@ def _validate_run_input(input_option: str | None, input_text: str | None) -> str
570
682
  final_input_text = input_option if input_option else input_text
571
683
 
572
684
  if not final_input_text:
573
- raise click.ClickException(
574
- "Input text is required. Use either positional argument or --input option."
575
- )
685
+ raise click.ClickException("Input text is required. Use either positional argument or --input option.")
576
686
 
577
687
  return final_input_text
578
688
 
@@ -584,8 +694,8 @@ def _parse_chat_history(chat_history: str | None) -> list[dict[str, Any]] | None
584
694
 
585
695
  try:
586
696
  return json.loads(chat_history)
587
- except json.JSONDecodeError:
588
- 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
589
699
 
590
700
 
591
701
  def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
@@ -665,9 +775,7 @@ def _save_run_transcript(save: str | None, result: Any, working_console: Any) ->
665
775
  if ext == "json":
666
776
  save_data = {
667
777
  "output": result or "",
668
- "full_debug_output": getattr(
669
- working_console, "get_captured_output", lambda: ""
670
- )(),
778
+ "full_debug_output": getattr(working_console, "get_captured_output", lambda: "")(),
671
779
  "timestamp": "captured during agent execution",
672
780
  }
673
781
  content = json.dumps(save_data, indent=2)
@@ -680,9 +788,7 @@ def _save_run_transcript(save: str | None, result: Any, working_console: Any) ->
680
788
 
681
789
  with open(save, "w", encoding="utf-8") as f:
682
790
  f.write(content)
683
- print_markup(
684
- f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console
685
- )
791
+ print_markup(f"[{SUCCESS_STYLE}]Full debug output saved to: {save}[/]", console=console)
686
792
 
687
793
 
688
794
  @agents_group.command()
@@ -728,10 +834,11 @@ def run(
728
834
  files: tuple[str, ...] | None,
729
835
  verbose: bool,
730
836
  ) -> None:
731
- """Run an agent with input text.
837
+ r"""Run an agent with input text.
732
838
 
733
839
  Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
734
840
 
841
+ \b
735
842
  Examples:
736
843
  aip agents run my-agent "Hello world"
737
844
  aip agents run agent-123 "Process this data" --timeout 600
@@ -745,9 +852,7 @@ def run(
745
852
 
746
853
  try:
747
854
  client = get_client(ctx)
748
- agent = _resolve_agent(
749
- ctx, client, agent_ref, select, interface_preference="fuzzy"
750
- )
855
+ agent = _resolve_agent(ctx, client, agent_ref, select, interface_preference="fuzzy")
751
856
 
752
857
  parsed_chat_history = _parse_chat_history(chat_history)
753
858
  renderer, working_console = _setup_run_renderer(ctx, save, verbose)
@@ -796,17 +901,19 @@ def run(
796
901
  except AgentTimeoutError as e:
797
902
  error_msg = str(e)
798
903
  handle_json_output(ctx, error=Exception(error_msg))
799
- raise click.ClickException(error_msg)
904
+ raise click.ClickException(error_msg) from e
800
905
  except Exception as e:
801
906
  _handle_command_exception(ctx, e)
802
907
 
803
908
 
804
909
  def _running_in_slash_mode(ctx: Any) -> bool:
910
+ """Return True if the command is executing inside the slash session."""
805
911
  ctx_obj = getattr(ctx, "obj", None)
806
912
  return isinstance(ctx_obj, dict) and bool(ctx_obj.get("_slash_session"))
807
913
 
808
914
 
809
915
  def _emit_verbose_guidance(ctx: Any) -> None:
916
+ """Explain the modern alternative to the deprecated --verbose flag."""
810
917
  if _running_in_slash_mode(ctx):
811
918
  message = (
812
919
  "[dim]Tip:[/] Verbose streaming has been retired in the command palette. Run the agent normally and open "
@@ -885,16 +992,12 @@ def _extract_and_validate_fields(
885
992
  if not name:
886
993
  raise click.ClickException("Agent name is required (--name or --import)")
887
994
  if not instruction:
888
- raise click.ClickException(
889
- "Agent instruction is required (--instruction or --import)"
890
- )
995
+ raise click.ClickException("Agent instruction is required (--instruction or --import)")
891
996
 
892
997
  return name, instruction, model, tools, agents, mcps, timeout
893
998
 
894
999
 
895
- def _validate_and_coerce_fields(
896
- name: str, instruction: str, timeout: Any
897
- ) -> tuple[str, str, Any]:
1000
+ def _validate_and_coerce_fields(name: str, instruction: str, timeout: Any) -> tuple[str, str, Any]:
898
1001
  """Validate and coerce field values."""
899
1002
  name = validate_agent_name(name)
900
1003
  instruction = validate_agent_instruction(instruction)
@@ -905,19 +1008,11 @@ def _validate_and_coerce_fields(
905
1008
  return name, instruction, timeout
906
1009
 
907
1010
 
908
- def _resolve_resources(
909
- client: Any, tools: tuple, agents: tuple, mcps: tuple
910
- ) -> tuple[list, list, list]:
1011
+ def _resolve_resources(client: Any, tools: tuple, agents: tuple, mcps: tuple) -> tuple[list, list, list]:
911
1012
  """Resolve tool, agent, and MCP references."""
912
- resolved_tools = _resolve_resources_by_name(
913
- client, tools, "tool", client.find_tools, "Tool"
914
- )
915
- resolved_agents = _resolve_resources_by_name(
916
- client, agents, "agent", client.find_agents, "Agent"
917
- )
918
- resolved_mcps = _resolve_resources_by_name(
919
- client, mcps, "mcp", client.find_mcps, "MCP"
920
- )
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")
921
1016
 
922
1017
  return resolved_tools, resolved_agents, resolved_mcps
923
1018
 
@@ -944,16 +1039,12 @@ def _build_create_kwargs(
944
1039
  }
945
1040
 
946
1041
  # Handle language model selection
947
- lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(
948
- merged_data, model
949
- )
1042
+ lm_selection_dict, should_strip_lm_identity = resolve_language_model_selection(merged_data, model)
950
1043
  create_kwargs.update(lm_selection_dict)
951
1044
 
952
1045
  # Handle import file specific logic
953
1046
  if import_file:
954
- _add_import_file_attributes(
955
- create_kwargs, merged_data, should_strip_lm_identity
956
- )
1047
+ _add_import_file_attributes(create_kwargs, merged_data, should_strip_lm_identity)
957
1048
 
958
1049
  return create_kwargs
959
1050
 
@@ -999,12 +1090,7 @@ def _get_language_model_display_name(agent: Any, model: str | None) -> str:
999
1090
  lm_display = getattr(agent, "model", None)
1000
1091
  if not lm_display:
1001
1092
  cfg = getattr(agent, "agent_config", {}) or {}
1002
- lm_display = (
1003
- cfg.get("lm_name")
1004
- or cfg.get("model")
1005
- or model
1006
- or f"{DEFAULT_MODEL} (backend default)"
1007
- )
1093
+ lm_display = cfg.get("lm_name") or cfg.get("model") or model or f"{DEFAULT_MODEL} (backend default)"
1008
1094
  return lm_display
1009
1095
 
1010
1096
 
@@ -1039,7 +1125,7 @@ def _handle_command_exception(ctx: Any, e: Exception) -> None:
1039
1125
  handle_json_output(ctx, error=e)
1040
1126
  if get_ctx_value(ctx, "view") != "json":
1041
1127
  print_api_error(e)
1042
- raise click.ClickException(str(e))
1128
+ raise click.exceptions.Exit(1) from e
1043
1129
 
1044
1130
 
1045
1131
  def _handle_creation_exception(ctx: Any, e: Exception) -> None:
@@ -1082,8 +1168,9 @@ def create(
1082
1168
  timeout: float | None,
1083
1169
  import_file: str | None,
1084
1170
  ) -> None:
1085
- """Create a new agent.
1171
+ r"""Create a new agent.
1086
1172
 
1173
+ \b
1087
1174
  Examples:
1088
1175
  aip agents create --name "My Agent" --instruction "You are a helpful assistant"
1089
1176
  aip agents create --import agent.json
@@ -1093,13 +1180,9 @@ def create(
1093
1180
 
1094
1181
  # Handle import file or CLI args
1095
1182
  if import_file:
1096
- merged_data = _handle_import_file_logic(
1097
- import_file, model, name, instruction, tools, agents, mcps, timeout
1098
- )
1183
+ merged_data = _handle_import_file_logic(import_file, model, name, instruction, tools, agents, mcps, timeout)
1099
1184
  else:
1100
- merged_data = _build_cli_args_data(
1101
- name, instruction, model, tools, agents, mcps, timeout
1102
- )
1185
+ merged_data = _build_cli_args_data(name, instruction, model, tools, agents, mcps, timeout)
1103
1186
 
1104
1187
  # Extract and validate fields
1105
1188
  (
@@ -1111,14 +1194,10 @@ def create(
1111
1194
  mcps,
1112
1195
  timeout,
1113
1196
  ) = _extract_and_validate_fields(merged_data)
1114
- name, instruction, timeout = _validate_and_coerce_fields(
1115
- name, instruction, timeout
1116
- )
1197
+ name, instruction, timeout = _validate_and_coerce_fields(name, instruction, timeout)
1117
1198
 
1118
1199
  # Resolve resources
1119
- resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(
1120
- client, tools, agents, mcps
1121
- )
1200
+ resolved_tools, resolved_agents, resolved_mcps = _resolve_resources(client, tools, agents, mcps)
1122
1201
 
1123
1202
  # Build create kwargs
1124
1203
  create_kwargs = _build_create_kwargs(
@@ -1148,7 +1227,7 @@ def _get_agent_for_update(client: Any, agent_id: str) -> Any:
1148
1227
  try:
1149
1228
  return client.agents.get_agent_by_id(agent_id)
1150
1229
  except Exception as e:
1151
- 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
1152
1231
 
1153
1232
 
1154
1233
  def _handle_update_import_file(
@@ -1230,16 +1309,12 @@ def _handle_update_import_config(
1230
1309
  if not import_file:
1231
1310
  return
1232
1311
 
1233
- lm_selection, should_strip_lm_identity = resolve_language_model_selection(
1234
- merged_data, None
1235
- )
1312
+ lm_selection, should_strip_lm_identity = resolve_language_model_selection(merged_data, None)
1236
1313
  update_data.update(lm_selection)
1237
1314
 
1238
1315
  raw_cfg = merged_data.get("agent_config") if isinstance(merged_data, dict) else None
1239
1316
  if isinstance(raw_cfg, dict):
1240
- update_data["agent_config"] = sanitize_agent_config(
1241
- raw_cfg, strip_lm_identity=should_strip_lm_identity
1242
- )
1317
+ update_data["agent_config"] = sanitize_agent_config(raw_cfg, strip_lm_identity=should_strip_lm_identity)
1243
1318
 
1244
1319
  excluded_fields = {
1245
1320
  "name",
@@ -1289,8 +1364,9 @@ def update(
1289
1364
  timeout: float | None,
1290
1365
  import_file: str | None,
1291
1366
  ) -> None:
1292
- """Update an existing agent.
1367
+ r"""Update an existing agent.
1293
1368
 
1369
+ \b
1294
1370
  Examples:
1295
1371
  aip agents update my-agent --instruction "New instruction"
1296
1372
  aip agents update my-agent --import agent.json
@@ -1308,16 +1384,18 @@ def update(
1308
1384
  agents,
1309
1385
  mcps,
1310
1386
  timeout,
1311
- ) = _handle_update_import_file(
1312
- import_file, name, instruction, tools, agents, mcps, timeout
1313
- )
1387
+ ) = _handle_update_import_file(import_file, name, instruction, tools, agents, mcps, timeout)
1314
1388
 
1315
- update_data = _build_update_data(
1316
- name, instruction, tools, agents, mcps, timeout
1317
- )
1389
+ update_data = _build_update_data(name, instruction, tools, agents, mcps, timeout)
1318
1390
 
1319
1391
  if merged_data:
1320
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
1321
1399
 
1322
1400
  if not update_data:
1323
1401
  raise click.ClickException("No update fields specified")
@@ -1352,7 +1430,7 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1352
1430
  try:
1353
1431
  agent = client.agents.get_agent_by_id(agent_id)
1354
1432
  except Exception as e:
1355
- 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
1356
1434
 
1357
1435
  # Confirm deletion when not forced
1358
1436
  if not yes and not display_confirmation_prompt("Agent", agent.name):
@@ -1384,13 +1462,11 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1384
1462
  "--base-url",
1385
1463
  help="Custom LangFlow server base URL (overrides LANGFLOW_BASE_URL env var)",
1386
1464
  )
1387
- @click.option(
1388
- "--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)"
1389
- )
1465
+ @click.option("--api-key", help="Custom LangFlow API key (overrides LANGFLOW_API_KEY env var)")
1390
1466
  @output_flags()
1391
1467
  @click.pass_context
1392
1468
  def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1393
- """Sync agents with LangFlow server flows.
1469
+ r"""Sync agents with LangFlow server flows.
1394
1470
 
1395
1471
  This command fetches all flows from the configured LangFlow server and
1396
1472
  creates/updates corresponding agents in the platform.
@@ -1399,6 +1475,7 @@ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1399
1475
  - Command options (--base-url, --api-key)
1400
1476
  - Environment variables (LANGFLOW_BASE_URL, LANGFLOW_API_KEY)
1401
1477
 
1478
+ \b
1402
1479
  Examples:
1403
1480
  aip agents sync-langflow
1404
1481
  aip agents sync-langflow --base-url https://my-langflow.com --api-key my-key
@@ -1415,15 +1492,16 @@ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1415
1492
  # Show success message for non-JSON output
1416
1493
  if get_ctx_value(ctx, "view") != "json":
1417
1494
  # Extract some useful info from the result
1418
- success_count = result.get("data", {}).get("created_count", 0) + result.get(
1419
- "data", {}
1420
- ).get("updated_count", 0)
1495
+ success_count = result.get("data", {}).get("created_count", 0) + result.get("data", {}).get(
1496
+ "updated_count", 0
1497
+ )
1421
1498
  total_count = result.get("data", {}).get("total_processed", 0)
1422
1499
 
1423
1500
  handle_rich_output(
1424
1501
  ctx,
1425
1502
  markup_text(
1426
- 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)[/]"
1427
1505
  ),
1428
1506
  )
1429
1507