glaip-sdk 0.0.2__py3-none-any.whl → 0.0.4__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 (40) hide show
  1. glaip_sdk/__init__.py +2 -2
  2. glaip_sdk/_version.py +51 -0
  3. glaip_sdk/branding.py +145 -0
  4. glaip_sdk/cli/commands/agents.py +876 -166
  5. glaip_sdk/cli/commands/configure.py +46 -104
  6. glaip_sdk/cli/commands/init.py +43 -118
  7. glaip_sdk/cli/commands/mcps.py +86 -161
  8. glaip_sdk/cli/commands/tools.py +196 -57
  9. glaip_sdk/cli/main.py +43 -29
  10. glaip_sdk/cli/utils.py +258 -27
  11. glaip_sdk/client/__init__.py +54 -2
  12. glaip_sdk/client/agents.py +196 -237
  13. glaip_sdk/client/base.py +62 -2
  14. glaip_sdk/client/mcps.py +63 -20
  15. glaip_sdk/client/tools.py +236 -81
  16. glaip_sdk/config/constants.py +10 -3
  17. glaip_sdk/exceptions.py +13 -0
  18. glaip_sdk/models.py +21 -5
  19. glaip_sdk/utils/__init__.py +116 -18
  20. glaip_sdk/utils/client_utils.py +284 -0
  21. glaip_sdk/utils/rendering/__init__.py +1 -0
  22. glaip_sdk/utils/rendering/formatting.py +211 -0
  23. glaip_sdk/utils/rendering/models.py +53 -0
  24. glaip_sdk/utils/rendering/renderer/__init__.py +38 -0
  25. glaip_sdk/utils/rendering/renderer/base.py +827 -0
  26. glaip_sdk/utils/rendering/renderer/config.py +33 -0
  27. glaip_sdk/utils/rendering/renderer/console.py +54 -0
  28. glaip_sdk/utils/rendering/renderer/debug.py +82 -0
  29. glaip_sdk/utils/rendering/renderer/panels.py +123 -0
  30. glaip_sdk/utils/rendering/renderer/progress.py +118 -0
  31. glaip_sdk/utils/rendering/renderer/stream.py +198 -0
  32. glaip_sdk/utils/rendering/steps.py +168 -0
  33. glaip_sdk/utils/run_renderer.py +22 -1086
  34. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/METADATA +8 -36
  35. glaip_sdk-0.0.4.dist-info/RECORD +41 -0
  36. glaip_sdk/cli/config.py +0 -592
  37. glaip_sdk/utils.py +0 -167
  38. glaip_sdk-0.0.2.dist-info/RECORD +0 -28
  39. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/WHEEL +0 -0
  40. {glaip_sdk-0.0.2.dist-info → glaip_sdk-0.0.4.dist-info}/entry_points.txt +0 -0
@@ -1,63 +1,538 @@
1
- """Agent management commands.
1
+ """Agent CLI commands for AIP SDK.
2
2
 
3
3
  Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
7
  import json
8
+ import os
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Any
8
12
 
9
13
  import click
10
14
  from rich.console import Console
11
15
  from rich.panel import Panel
12
16
  from rich.text import Text
13
17
 
18
+ from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT, DEFAULT_MODEL
19
+ from glaip_sdk.exceptions import AgentTimeoutError
14
20
  from glaip_sdk.utils import is_uuid
15
21
 
16
22
  from ..utils import (
23
+ _fuzzy_pick_for_resources,
24
+ build_renderer,
25
+ coerce_to_row,
17
26
  get_client,
18
- handle_ambiguous_resource,
19
27
  output_flags,
20
28
  output_list,
21
29
  output_result,
22
- safe_getattr,
30
+ resolve_resource,
23
31
  )
24
32
 
25
33
  console = Console()
26
34
 
27
35
 
36
+ def _display_run_suggestions(agent):
37
+ """Display helpful suggestions for running the agent."""
38
+ console.print()
39
+ console.print(
40
+ Panel(
41
+ f"[bold blue]💡 Next Steps:[/bold blue]\n\n"
42
+ f"🚀 Run this agent:\n"
43
+ f' [green]aip agents run {agent.id} "Your message here"[/green]\n\n'
44
+ f"📋 Or use the agent name:\n"
45
+ f' [green]aip agents run "{agent.name}" "Your message here"[/green]\n\n'
46
+ f"🔧 Available options:\n"
47
+ f" [dim]--chat-history[/dim] Include previous conversation\n"
48
+ f" [dim]--file[/dim] Attach files\n"
49
+ f" [dim]--input[/dim] Alternative input method\n"
50
+ f" [dim]--timeout[/dim] Set execution timeout\n"
51
+ f" [dim]--save[/dim] Save transcript to file\n"
52
+ f" [dim]--verbose[/dim] Show detailed execution\n\n"
53
+ f"💡 [dim]Input text can be positional OR use --input flag (both work!)[/dim]",
54
+ title="🤖 Ready to Run Agent",
55
+ border_style="blue",
56
+ padding=(0, 1),
57
+ )
58
+ )
59
+
60
+
61
+ def _format_datetime(dt):
62
+ """Format datetime object to readable string."""
63
+ if isinstance(dt, datetime):
64
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
65
+ elif dt is None:
66
+ return "N/A"
67
+ return dt
68
+
69
+
70
+ def _fetch_full_agent_details(client, agent):
71
+ """Fetch full agent details by ID to ensure all fields are populated."""
72
+ try:
73
+ agent_id = str(getattr(agent, "id", "")).strip()
74
+ if agent_id:
75
+ return client.agents.get_agent_by_id(agent_id)
76
+ except Exception:
77
+ # If fetching full details fails, continue with the resolved object
78
+ pass
79
+ return agent
80
+
81
+
82
+ def _build_agent_result_data(agent):
83
+ """Build standardized result data for agent display."""
84
+ return {
85
+ "id": str(getattr(agent, "id", "N/A")),
86
+ "name": getattr(agent, "name", "N/A"),
87
+ "type": getattr(agent, "type", "N/A"),
88
+ "framework": getattr(agent, "framework", "N/A"),
89
+ "version": getattr(agent, "version", "N/A"),
90
+ "description": getattr(agent, "description", "N/A"),
91
+ "instruction": getattr(agent, "instruction", "") or "-",
92
+ "created_at": _format_datetime(getattr(agent, "created_at", "N/A")),
93
+ "updated_at": _format_datetime(getattr(agent, "updated_at", "N/A")),
94
+ "metadata": getattr(agent, "metadata", "N/A"),
95
+ "language_model_id": getattr(agent, "language_model_id", "N/A"),
96
+ "agent_config": getattr(agent, "agent_config", "N/A"),
97
+ "tool_configs": getattr(agent, "tool_configs", {}),
98
+ "tools": getattr(agent, "tools", []),
99
+ "agents": getattr(agent, "agents", []),
100
+ "mcps": getattr(agent, "mcps", []),
101
+ "a2a_profile": getattr(agent, "a2a_profile", "N/A"),
102
+ }
103
+
104
+
105
+ def _get_agent_attributes(agent):
106
+ """Dynamically get all relevant agent attributes for export."""
107
+ # Exclude these attributes from export (methods, private attrs, computed properties)
108
+ exclude_attrs = {
109
+ "id",
110
+ "created_at",
111
+ "updated_at", # System-managed fields
112
+ "_client",
113
+ "_raw_data", # Internal fields
114
+ }
115
+
116
+ # Methods and callable attributes to exclude
117
+ exclude_callables = {
118
+ "model_dump",
119
+ "dict",
120
+ "json", # Pydantic methods
121
+ "get",
122
+ "post",
123
+ "put",
124
+ "delete", # HTTP methods
125
+ "save",
126
+ "refresh",
127
+ "update", # ORM methods
128
+ }
129
+
130
+ export_data = {}
131
+
132
+ # Method 1: Try Pydantic model_dump() if available (best for structured data)
133
+ if hasattr(agent, "model_dump") and callable(agent.model_dump):
134
+ try:
135
+ # Get all model fields
136
+ all_data = agent.model_dump()
137
+ # Filter out excluded attributes
138
+ for key, value in all_data.items():
139
+ if key not in exclude_attrs and key not in exclude_callables:
140
+ export_data[key] = value
141
+ return export_data
142
+ except Exception:
143
+ # Fall back to manual attribute detection
144
+ pass
145
+
146
+ # Method 2: Manual attribute inspection with filtering
147
+ # Get all non-private, non-method attributes
148
+ all_attrs = []
149
+ if hasattr(agent, "__dict__"):
150
+ all_attrs.extend(agent.__dict__.keys())
151
+ if hasattr(agent, "__annotations__"):
152
+ all_attrs.extend(agent.__annotations__.keys())
153
+
154
+ # Remove duplicates and filter
155
+ all_attrs = list(set(all_attrs))
156
+
157
+ for attr in all_attrs:
158
+ # Skip excluded attributes
159
+ if (
160
+ attr in exclude_attrs
161
+ or attr.startswith("_") # Private attributes
162
+ or attr in exclude_callables
163
+ ):
164
+ continue
165
+
166
+ # Skip callable attributes (methods, functions)
167
+ attr_value = getattr(agent, attr, None)
168
+ if callable(attr_value):
169
+ continue
170
+
171
+ # Add the attribute to export data
172
+ export_data[attr] = attr_value
173
+
174
+ return export_data
175
+
176
+
177
+ def _build_agent_export_data(agent):
178
+ """Build comprehensive export data for agent (always includes all fields)."""
179
+ # Get all available agent attributes dynamically
180
+ all_agent_data = _get_agent_attributes(agent)
181
+
182
+ # Always include all detected attributes for comprehensive export
183
+ # Add default timeout if not present
184
+ if "timeout" not in all_agent_data:
185
+ all_agent_data["timeout"] = DEFAULT_AGENT_RUN_TIMEOUT
186
+ return all_agent_data
187
+
188
+
189
+ def _get_agent_model_name(agent):
190
+ """Extract model name from agent configuration."""
191
+ # Try different possible locations for model name
192
+ if hasattr(agent, "agent_config") and agent.agent_config:
193
+ if isinstance(agent.agent_config, dict):
194
+ return agent.agent_config.get("lm_name") or agent.agent_config.get("model")
195
+
196
+ if hasattr(agent, "model") and agent.model:
197
+ return agent.model
198
+
199
+ # Default fallback
200
+ return DEFAULT_MODEL
201
+
202
+
203
+ def _extract_tool_ids(agent):
204
+ """Extract tool IDs from agent tools list for import compatibility."""
205
+ tools = getattr(agent, "tools", [])
206
+ if not tools:
207
+ return []
208
+
209
+ ids = []
210
+ for tool in tools:
211
+ if isinstance(tool, dict):
212
+ tool_id = tool.get("id")
213
+ if tool_id:
214
+ ids.append(tool_id)
215
+ else:
216
+ # Fallback to name if ID not available
217
+ name = tool.get("name", "")
218
+ if name:
219
+ ids.append(name)
220
+ elif hasattr(tool, "id"):
221
+ ids.append(tool.id)
222
+ elif hasattr(tool, "name"):
223
+ ids.append(tool.name)
224
+ else:
225
+ ids.append(str(tool))
226
+ return ids
227
+
228
+
229
+ def _extract_agent_ids(agent):
230
+ """Extract agent IDs from agent agents list for import compatibility."""
231
+ agents = getattr(agent, "agents", [])
232
+ if not agents:
233
+ return []
234
+
235
+ ids = []
236
+ for sub_agent in agents:
237
+ if isinstance(sub_agent, dict):
238
+ agent_id = sub_agent.get("id")
239
+ if agent_id:
240
+ ids.append(agent_id)
241
+ else:
242
+ # Fallback to name if ID not available
243
+ name = sub_agent.get("name", "")
244
+ if name:
245
+ ids.append(name)
246
+ elif hasattr(sub_agent, "id"):
247
+ ids.append(sub_agent.id)
248
+ elif hasattr(sub_agent, "name"):
249
+ ids.append(sub_agent.name)
250
+ else:
251
+ ids.append(str(sub_agent))
252
+ return ids
253
+
254
+
255
+ def _export_agent_to_file(agent, file_path: Path, format: str = "json"):
256
+ """Export agent to file (JSON or YAML) with comprehensive data."""
257
+ export_data = _build_agent_export_data(agent)
258
+
259
+ if format.lower() == "yaml" or file_path.suffix.lower() in [".yaml", ".yml"]:
260
+ _write_yaml_to_file(file_path, export_data)
261
+ else:
262
+ _write_json_to_file(file_path, export_data)
263
+
264
+
265
+ def _load_agent_from_file(file_path: Path) -> dict[str, Any]:
266
+ """Load agent data from JSON or YAML file."""
267
+ if file_path.suffix.lower() in [".yaml", ".yml"]:
268
+ return _read_yaml_from_file(file_path)
269
+ else:
270
+ return _read_json_from_file(file_path)
271
+
272
+
273
+ def _read_yaml_from_file(file_path: Path) -> dict[str, Any]:
274
+ """Read YAML data from file."""
275
+ try:
276
+ import yaml
277
+ except ImportError:
278
+ raise click.ClickException(
279
+ "PyYAML is required for YAML import. Install with: pip install PyYAML"
280
+ )
281
+
282
+ if not file_path.exists():
283
+ raise FileNotFoundError(f"File not found: {file_path}")
284
+
285
+ with open(file_path, encoding="utf-8") as f:
286
+ data = yaml.safe_load(f)
287
+
288
+ # Handle instruction_lines array format for user-friendly YAML
289
+ if "instruction_lines" in data and isinstance(data["instruction_lines"], list):
290
+ data["instruction"] = "\n\n".join(data["instruction_lines"])
291
+ del data["instruction_lines"]
292
+
293
+ # Handle instruction as list from YAML export (convert back to string)
294
+ if "instruction" in data and isinstance(data["instruction"], list):
295
+ data["instruction"] = "\n\n".join(data["instruction"])
296
+
297
+ return data
298
+
299
+
300
+ def _extract_ids_from_export(items: list) -> list[str]:
301
+ """Extract IDs from export format (list of dicts with id/name fields)."""
302
+ ids = []
303
+ for item in items:
304
+ if isinstance(item, dict):
305
+ item_id = item.get("id")
306
+ if item_id:
307
+ ids.append(item_id)
308
+ elif isinstance(item, str):
309
+ ids.append(item)
310
+ return ids
311
+
312
+
313
+ def _convert_export_to_import_format(data: dict[str, Any]) -> dict[str, Any]:
314
+ """Convert export format to import-compatible format (extract IDs from objects)."""
315
+ import_data = data.copy()
316
+
317
+ # Convert tools from dicts to IDs
318
+ if "tools" in import_data and isinstance(import_data["tools"], list):
319
+ import_data["tools"] = _extract_ids_from_export(import_data["tools"])
320
+
321
+ # Convert agents from dicts to IDs
322
+ if "agents" in import_data and isinstance(import_data["agents"], list):
323
+ import_data["agents"] = _extract_ids_from_export(import_data["agents"])
324
+
325
+ return import_data
326
+
327
+
328
+ def _write_json_to_file(file_path: Path, data: dict[str, Any], indent: int = 2) -> None:
329
+ """Write data to JSON file with consistent formatting."""
330
+ with open(file_path, "w", encoding="utf-8") as f:
331
+ json.dump(data, f, indent=indent)
332
+
333
+
334
+ def _write_yaml_to_file(file_path: Path, data: dict[str, Any]) -> None:
335
+ """Write data to YAML file with user-friendly formatting."""
336
+ try:
337
+ import yaml
338
+ except ImportError:
339
+ raise click.ClickException(
340
+ "PyYAML is required for YAML export. Install with: pip install PyYAML"
341
+ )
342
+
343
+ # Custom YAML dumper for user-friendly instruction formatting
344
+ class LiteralString(str):
345
+ pass
346
+
347
+ def literal_string_representer(dumper, data):
348
+ # Use literal block scalar (|) for multiline strings to preserve formatting
349
+ if "\n" in data:
350
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
351
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
352
+
353
+ # Add custom representer to the YAML dumper
354
+ yaml.add_representer(LiteralString, literal_string_representer)
355
+
356
+ # Convert instruction to LiteralString for proper formatting
357
+ if "instruction" in data and data["instruction"]:
358
+ data["instruction"] = LiteralString(data["instruction"])
359
+
360
+ with open(file_path, "w", encoding="utf-8") as f:
361
+ yaml.dump(
362
+ data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
363
+ )
364
+
365
+
366
+ def _read_json_from_file(file_path: Path) -> dict[str, Any]:
367
+ """Read JSON data from file with validation."""
368
+ if not file_path.exists():
369
+ raise FileNotFoundError(f"File not found: {file_path}")
370
+
371
+ if file_path.suffix.lower() != ".json":
372
+ raise ValueError(
373
+ f"Unsupported file format: {file_path.suffix}. Only JSON files are supported."
374
+ )
375
+
376
+ with open(file_path, encoding="utf-8") as f:
377
+ return json.load(f)
378
+
379
+
380
+ def _merge_import_with_cli_args(
381
+ import_data: dict[str, Any],
382
+ cli_args: dict[str, Any],
383
+ array_fields: list[str] = None,
384
+ ) -> dict[str, Any]:
385
+ """Merge imported data with CLI arguments, preferring CLI args.
386
+
387
+ Args:
388
+ import_data: Data loaded from import file
389
+ cli_args: Arguments passed via CLI
390
+ array_fields: Fields that should be combined (merged) rather than replaced
391
+
392
+ Returns:
393
+ Merged data dictionary
394
+ """
395
+ if array_fields is None:
396
+ array_fields = ["tools", "agents"]
397
+
398
+ merged = {}
399
+
400
+ for key, cli_value in cli_args.items():
401
+ if cli_value is not None and (
402
+ not isinstance(cli_value, list | tuple) or len(cli_value) > 0
403
+ ):
404
+ # CLI value takes precedence (for non-empty values)
405
+ if key in array_fields and key in import_data:
406
+ # For array fields, combine CLI and imported values
407
+ import_value = import_data[key]
408
+ if isinstance(import_value, list):
409
+ merged[key] = list(cli_value) + import_value
410
+ else:
411
+ merged[key] = cli_value
412
+ else:
413
+ merged[key] = cli_value
414
+ elif key in import_data:
415
+ # Use imported value if no CLI value
416
+ merged[key] = import_data[key]
417
+
418
+ # Add any import-only fields
419
+ for key, import_value in import_data.items():
420
+ if key not in merged:
421
+ merged[key] = import_value
422
+
423
+ return merged
424
+
425
+
426
+ def _resolve_resources_by_name(
427
+ client, items: tuple[str, ...], resource_type: str, find_func, label: str
428
+ ) -> list[str]:
429
+ """Resolve resource names/IDs to IDs, handling ambiguity.
430
+
431
+ Args:
432
+ client: API client
433
+ items: Tuple of resource names/IDs
434
+ resource_type: Type of resource ("tool" or "agent")
435
+ find_func: Function to find resources by name
436
+ label: Label for error messages
437
+
438
+ Returns:
439
+ List of resolved resource IDs
440
+ """
441
+ out = []
442
+ for ref in list(items or ()):
443
+ if is_uuid(ref):
444
+ out.append(ref)
445
+ continue
446
+
447
+ matches = find_func(name=ref)
448
+ if not matches:
449
+ raise click.ClickException(f"{label} not found: {ref}")
450
+ if len(matches) > 1:
451
+ raise click.ClickException(
452
+ f"Multiple {resource_type}s named '{ref}'. Use ID instead."
453
+ )
454
+ out.append(str(matches[0].id))
455
+ return out
456
+
457
+
458
+ def _display_agent_creation_success(agent, model=None, default_model=DEFAULT_MODEL):
459
+ """Display success message for agent creation/update."""
460
+ lm = getattr(agent, "model", None)
461
+ if not lm:
462
+ cfg = getattr(agent, "agent_config", {}) or {}
463
+ lm = (
464
+ cfg.get("lm_name")
465
+ or cfg.get("model")
466
+ or model # Use provided model if specified
467
+ or f"{default_model} (backend default)"
468
+ )
469
+
470
+ panel = Panel(
471
+ f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
472
+ f"ID: {agent.id}\n"
473
+ f"Model: {lm}\n"
474
+ f"Type: {getattr(agent, 'type', 'config')}\n"
475
+ f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
476
+ f"Version: {getattr(agent, 'version', '1.0')}",
477
+ title="🤖 Agent Created",
478
+ border_style="green",
479
+ padding=(0, 1),
480
+ )
481
+ console.print(panel)
482
+
483
+
484
+ def _display_agent_update_success(agent):
485
+ """Display success message for agent update."""
486
+ console.print(Text(f"[green]✅ Agent '{agent.name}' updated successfully[/green]"))
487
+
488
+
489
+ def _display_agent_details(ctx, client, agent):
490
+ """Display full agent details in a standardized format."""
491
+ # Fetch full details to ensure all fields are populated
492
+ full_agent = _fetch_full_agent_details(client, agent)
493
+
494
+ # Build result data
495
+ result_data = _build_agent_result_data(full_agent)
496
+
497
+ # Display using output_result
498
+ output_result(
499
+ ctx,
500
+ result_data,
501
+ title="Agent Details",
502
+ panel_title=f"🤖 {full_agent.name}",
503
+ )
504
+
505
+
28
506
  @click.group(name="agents", no_args_is_help=True)
29
507
  def agents_group():
30
508
  """Agent management operations."""
31
509
  pass
32
510
 
33
511
 
34
- def _resolve_agent(ctx, client, ref, select=None):
35
- """Resolve agent reference (ID or name) with ambiguity handling."""
36
- if is_uuid(ref):
37
- return client.agents.get_agent_by_id(ref)
38
-
39
- # Find agents by name
40
- matches = client.agents.find_agents(name=ref)
41
- if not matches:
42
- raise click.ClickException(f"Agent '{ref}' not found")
43
-
44
- if len(matches) == 1:
45
- return matches[0]
46
-
47
- # Multiple matches - handle ambiguity
48
- if select:
49
- idx = int(select) - 1
50
- if not (0 <= idx < len(matches)):
51
- raise click.ClickException(f"--select must be 1..{len(matches)}")
52
- return matches[idx]
512
+ def _resolve_agent(ctx, client, ref, select=None, interface_preference="fuzzy"):
513
+ """Resolve agent reference (ID or name) with ambiguity handling.
53
514
 
54
- return handle_ambiguous_resource(ctx, "agent", ref, matches)
515
+ Args:
516
+ interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
517
+ """
518
+ return resolve_resource(
519
+ ctx,
520
+ ref,
521
+ get_by_id=client.agents.get_agent_by_id,
522
+ find_by_name=client.agents.find_agents,
523
+ label="Agent",
524
+ select=select,
525
+ interface_preference=interface_preference,
526
+ )
55
527
 
56
528
 
57
529
  @agents_group.command(name="list")
530
+ @click.option(
531
+ "--simple", is_flag=True, help="Show simple table without interactive picker"
532
+ )
58
533
  @output_flags()
59
534
  @click.pass_context
60
- def list_agents(ctx):
535
+ def list_agents(ctx, simple):
61
536
  """List all agents."""
62
537
  try:
63
538
  client = get_client(ctx)
@@ -74,14 +549,20 @@ def list_agents(ctx):
74
549
 
75
550
  # Transform function for safe attribute access
76
551
  def transform_agent(agent):
77
- return {
78
- "id": str(agent.id),
79
- "name": agent.name,
80
- "type": safe_getattr(agent, "type") or "N/A",
81
- "framework": safe_getattr(agent, "framework") or "N/A",
82
- "version": safe_getattr(agent, "version") or "N/A",
83
- }
552
+ row = coerce_to_row(agent, ["id", "name", "type", "framework", "version"])
553
+ # Ensure id is always a string
554
+ row["id"] = str(row["id"])
555
+ return row
84
556
 
557
+ # Use fuzzy picker for interactive agent selection and details (default behavior)
558
+ # Skip if --simple flag is used
559
+ if not simple and console.is_terminal and os.isatty(1) and len(agents) > 0:
560
+ picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
561
+ if picked_agent:
562
+ _display_agent_details(ctx, client, picked_agent)
563
+ return
564
+
565
+ # Show simple table (either --simple flag or non-interactive)
85
566
  output_list(ctx, agents, "🤖 Available Agents", columns, transform_agent)
86
567
 
87
568
  except Exception as e:
@@ -91,64 +572,84 @@ def list_agents(ctx):
91
572
  @agents_group.command()
92
573
  @click.argument("agent_ref")
93
574
  @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
575
+ @click.option(
576
+ "--export",
577
+ type=click.Path(dir_okay=False, writable=True),
578
+ help="Export complete agent configuration to file (format auto-detected from .json/.yaml extension)",
579
+ )
94
580
  @output_flags()
95
581
  @click.pass_context
96
- def get(ctx, agent_ref, select):
97
- """Get agent details."""
582
+ def get(ctx, agent_ref, select, export):
583
+ """Get agent details.
584
+
585
+ Examples:
586
+ aip agents get my-agent
587
+ aip agents get my-agent --export agent.json # Exports complete configuration as JSON
588
+ aip agents get my-agent --export agent.yaml # Exports complete configuration as YAML
589
+ """
98
590
  try:
99
591
  client = get_client(ctx)
100
592
 
101
- # Resolve agent with ambiguity handling
102
- agent = _resolve_agent(ctx, client, agent_ref, select)
103
-
104
- # Create result data with all available fields from backend
105
- result_data = {
106
- "id": str(getattr(agent, "id", "N/A")),
107
- "name": getattr(agent, "name", "N/A"),
108
- "type": getattr(agent, "type", "N/A"),
109
- "framework": getattr(agent, "framework", "N/A"),
110
- "version": getattr(agent, "version", "N/A"),
111
- "description": getattr(agent, "description", "N/A"),
112
- "instruction": getattr(agent, "instruction", "") or "-",
113
- "metadata": getattr(agent, "metadata", "N/A"),
114
- "language_model_id": getattr(agent, "language_model_id", "N/A"),
115
- "agent_config": getattr(agent, "agent_config", "N/A"),
116
- "tools": getattr(agent, "tools", []),
117
- "agents": getattr(agent, "agents", []),
118
- "mcps": getattr(agent, "mcps", []),
119
- "a2a_profile": getattr(agent, "a2a_profile", "N/A"),
120
- }
121
-
122
- output_result(
123
- ctx, result_data, title="Agent Details", panel_title=f"🤖 {agent.name}"
593
+ # Resolve agent with ambiguity handling - use questionary interface for traditional UX
594
+ agent = _resolve_agent(
595
+ ctx, client, agent_ref, select, interface_preference="questionary"
124
596
  )
125
597
 
598
+ # Handle export option
599
+ if export:
600
+ export_path = Path(export)
601
+ # Auto-detect format from file extension
602
+ if export_path.suffix.lower() in [".yaml", ".yml"]:
603
+ detected_format = "yaml"
604
+ else:
605
+ detected_format = "json"
606
+
607
+ # Always export comprehensive data - re-fetch agent with full details
608
+ try:
609
+ agent = client.agents.get_agent_by_id(agent.id)
610
+ except Exception as e:
611
+ console.print(
612
+ Text(f"[yellow]⚠️ Could not fetch full agent details: {e}[/yellow]")
613
+ )
614
+ console.print(
615
+ Text("[yellow]⚠️ Proceeding with available data[/yellow]")
616
+ )
617
+
618
+ _export_agent_to_file(agent, export_path, detected_format)
619
+ console.print(
620
+ Text(
621
+ f"[green]✅ Complete agent configuration exported to: {export_path} (format: {detected_format})[/green]"
622
+ )
623
+ )
624
+
625
+ # Display full agent details using the standardized helper
626
+ _display_agent_details(ctx, client, agent)
627
+
628
+ # Show run suggestions (only in rich mode, not JSON)
629
+ if ctx.obj.get("view") != "json":
630
+ _display_run_suggestions(agent)
631
+
126
632
  except Exception as e:
127
633
  raise click.ClickException(str(e))
128
634
 
129
635
 
130
636
  @agents_group.command()
131
- @click.argument("agent_id")
132
- @click.option("--input", "input_text", required=True, help="Input text for the agent")
637
+ @click.argument("agent_ref")
638
+ @click.argument("input_text", required=False)
639
+ @click.option("--select", type=int, help="Choose among ambiguous matches (1-based)")
640
+ @click.option("--input", "input_option", help="Input text for the agent")
133
641
  @click.option("--chat-history", help="JSON string of chat history")
134
- @click.option("--timeout", default=600, type=int, help="Request timeout in seconds")
135
642
  @click.option(
136
- "--view",
137
- type=click.Choice(["rich", "plain", "json", "md"]),
138
- default="rich",
139
- help="Output view format",
140
- )
141
- @click.option(
142
- "--compact/--verbose", default=True, help="Collapse tool steps (default: compact)"
643
+ "--timeout",
644
+ default=DEFAULT_AGENT_RUN_TIMEOUT,
645
+ type=int,
646
+ help="Agent execution timeout in seconds (default: 300s)",
143
647
  )
144
648
  @click.option(
145
649
  "--save",
146
650
  type=click.Path(dir_okay=False, writable=True),
147
651
  help="Save transcript to file (md or json)",
148
652
  )
149
- @click.option(
150
- "--theme", type=click.Choice(["dark", "light"]), default="dark", help="Color theme"
151
- )
152
653
  @click.option(
153
654
  "--file",
154
655
  "files",
@@ -156,28 +657,50 @@ def get(ctx, agent_ref, select):
156
657
  type=click.Path(exists=True),
157
658
  help="Attach file(s)",
158
659
  )
660
+ @click.option(
661
+ "--verbose/--no-verbose",
662
+ default=False,
663
+ help="Show detailed SSE events during streaming",
664
+ )
665
+ @output_flags()
159
666
  @click.pass_context
160
667
  def run(
161
668
  ctx,
162
- agent_id,
669
+ agent_ref,
670
+ select,
163
671
  input_text,
672
+ input_option,
164
673
  chat_history,
165
674
  timeout,
166
- view,
167
- compact,
168
675
  save,
169
- theme,
170
676
  files,
677
+ verbose,
171
678
  ):
172
- """Run an agent with input text (ID required)."""
679
+ """Run an agent with input text.
680
+
681
+ Usage: aip agents run <agent_ref> <input_text> [OPTIONS]
682
+
683
+ Examples:
684
+ aip agents run my-agent "Hello world"
685
+ aip agents run agent-123 "Process this data" --timeout 600
686
+ aip agents run my-agent --input "Hello world" # Legacy style
687
+ """
688
+ # Handle input precedence: --input option overrides positional argument
689
+ final_input_text = input_option if input_option else input_text
690
+
691
+ # Validate that we have input text from either positional argument or --input option
692
+ if not final_input_text:
693
+ raise click.ClickException(
694
+ "Input text is required. Use either positional argument or --input option."
695
+ )
696
+
173
697
  try:
174
698
  client = get_client(ctx)
175
699
 
176
- # Get agent by ID (no ambiguity handling needed)
177
- try:
178
- agent = client.agents.get_agent_by_id(agent_id)
179
- except Exception as e:
180
- raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
700
+ # Resolve agent by ID or name (align with other commands) - use fuzzy interface
701
+ agent = _resolve_agent(
702
+ ctx, client, agent_ref, select, interface_preference="fuzzy"
703
+ )
181
704
 
182
705
  # Parse chat history if provided
183
706
  parsed_chat_history = None
@@ -187,131 +710,255 @@ def run(
187
710
  except json.JSONDecodeError:
188
711
  raise click.ClickException("Invalid JSON in chat history")
189
712
 
190
- # Always stream (no --no-stream option)
191
- stream = ctx.obj.get("tty", True)
713
+ # Create custom renderer with CLI flags
714
+ tty_enabled = bool((ctx.obj or {}).get("tty", True))
192
715
 
193
- # Create appropriate renderer based on view
194
- renderer = None
195
- if stream:
196
- from ...utils.run_renderer import RichStreamRenderer
716
+ # Build renderer and capturing console
717
+ renderer, working_console = build_renderer(
718
+ ctx,
719
+ save_path=save,
720
+ verbose=verbose,
721
+ tty_enabled=tty_enabled,
722
+ )
197
723
 
198
- # Use RichStreamRenderer for all streaming output
199
- # Different view formats are handled in the output logic below
200
- renderer = RichStreamRenderer(
201
- console, verbose=not compact, theme=theme, use_emoji=True
202
- )
724
+ # Set HTTP timeout to match agent timeout exactly
725
+ # This ensures the agent timeout controls the HTTP timeout
726
+ try:
727
+ client.timeout = float(timeout)
728
+ except Exception:
729
+ pass
203
730
 
204
- # Run agent
205
- result = client.agents.run_agent(
206
- agent_id=agent.id,
207
- message=input_text,
208
- files=list(files),
209
- stream=stream,
210
- agent_name=agent.name, # Pass agent name for better display
211
- **({"chat_history": parsed_chat_history} if parsed_chat_history else {}),
212
- **({"timeout": timeout} if timeout else {}),
213
- )
731
+ # Ensure timeout is applied to the root client and subclients share its session
732
+ run_kwargs = {
733
+ "agent_id": agent.id,
734
+ "message": final_input_text,
735
+ "files": list(files),
736
+ "agent_name": agent.name, # Pass agent name for better display
737
+ "tty": tty_enabled,
738
+ }
739
+
740
+ # Add optional parameters
741
+ if parsed_chat_history:
742
+ run_kwargs["chat_history"] = parsed_chat_history
743
+
744
+ # Pass custom renderer if available
745
+ if renderer is not None:
746
+ run_kwargs["renderer"] = renderer
747
+
748
+ # Pass timeout to client (verbose mode is handled by the renderer)
749
+ result = client.agents.run_agent(**run_kwargs, timeout=timeout)
214
750
 
215
751
  # Check if renderer already printed output (for streaming renderers)
216
- printed_by_renderer = bool(renderer and stream)
752
+ # Note: Auto-paging is handled by the renderer when view=="rich"
753
+ printed_by_renderer = bool(renderer)
217
754
 
218
- # Handle output format for non-streaming or fallback
755
+ # Resolve selected view from context (output_flags() stores it here)
756
+ selected_view = (ctx.obj or {}).get("view", "rich")
757
+
758
+ # Handle output format for fallback
219
759
  # Only print here if nothing was printed by the renderer
220
760
  if not printed_by_renderer:
221
- if (ctx.obj.get("view") == "json") or (view == "json"):
761
+ if selected_view == "json":
222
762
  click.echo(json.dumps({"output": result}, indent=2))
223
- elif view == "md":
763
+ elif selected_view == "md":
224
764
  click.echo(f"# Assistant\n\n{result}")
225
- elif view == "plain":
765
+ elif selected_view == "plain":
226
766
  click.echo(result)
227
- elif not stream:
228
- # Rich output for non-streaming
229
- panel = Panel(
230
- Text(result, style="green"),
231
- title="Agent Output",
232
- border_style="green",
233
- )
234
- console.print(panel)
235
767
 
236
768
  # Save transcript if requested
237
- if save and result:
769
+ if save:
238
770
  ext = (save.rsplit(".", 1)[-1] or "").lower()
239
771
  if ext == "json":
240
- content = json.dumps({"output": result}, indent=2)
241
- with open(save, "w", encoding="utf-8") as f:
242
- f.write(content)
772
+ # Save both the result and captured output
773
+ save_data = {
774
+ "output": result or "",
775
+ "full_debug_output": getattr(
776
+ working_console, "get_captured_output", lambda: ""
777
+ )(),
778
+ "timestamp": "captured during agent execution",
779
+ }
780
+ content = json.dumps(save_data, indent=2)
243
781
  else:
244
- content = f"# Assistant\n\n{result}\n"
245
- with open(save, "w", encoding="utf-8") as f:
246
- f.write(content)
247
- console.print(f"[green]Transcript saved to: {save}[/green]")
782
+ # For markdown/text files, save the full captured output if available
783
+ # Get the full captured output including all tool panels and debug info (if available)
784
+ full_output = getattr(
785
+ working_console, "get_captured_output", lambda: ""
786
+ )()
787
+ if full_output:
788
+ content = f"# Agent Debug Log\n\n{full_output}\n\n---\n\n## Final Result\n\n{result or ''}\n"
789
+ else:
790
+ # Fallback to simple format
791
+ content = f"# Assistant\n\n{result or ''}\n"
792
+
793
+ with open(save, "w", encoding="utf-8") as f:
794
+ f.write(content)
795
+ console.print(Text(f"[green]Full debug output saved to: {save}[/green]"))
248
796
 
797
+ except AgentTimeoutError as e:
798
+ # Handle agent timeout errors with specific messages
799
+ error_msg = str(e)
800
+ if ctx.obj.get("view") == "json":
801
+ click.echo(json.dumps({"error": error_msg}, indent=2))
802
+ # Don't print the error message here - Click.ClickException will handle it
803
+ raise click.ClickException(error_msg)
249
804
  except Exception as e:
250
805
  if ctx.obj.get("view") == "json":
251
806
  click.echo(json.dumps({"error": str(e)}, indent=2))
252
- else:
253
- console.print(f"[red]Error running agent: {e}[/red]")
807
+ # Don't print the error message here - Click.ClickException will handle it
254
808
  raise click.ClickException(str(e))
255
809
 
256
810
 
257
811
  @agents_group.command()
258
- @click.option("--name", required=True, help="Agent name")
259
- @click.option("--instruction", required=True, help="Agent instruction (prompt)")
812
+ @click.option("--name", help="Agent name")
813
+ @click.option("--instruction", help="Agent instruction (prompt)")
814
+ @click.option(
815
+ "--model",
816
+ help=f"Language model to use (e.g., {DEFAULT_MODEL}, default: {DEFAULT_MODEL})",
817
+ )
260
818
  @click.option("--tools", multiple=True, help="Tool names or IDs to attach")
261
- @click.option("--agents", multiple=True, help="Sub-agent names to attach")
262
- @click.option("--timeout", default=300, type=int, help="Execution timeout in seconds")
819
+ @click.option("--agents", multiple=True, help="Sub-agent names or IDs to attach")
820
+ @click.option(
821
+ "--timeout",
822
+ default=DEFAULT_AGENT_RUN_TIMEOUT,
823
+ type=int,
824
+ help="Agent execution timeout in seconds (default: 300s)",
825
+ )
826
+ @click.option(
827
+ "--import",
828
+ "import_file",
829
+ type=click.Path(exists=True, dir_okay=False),
830
+ help="Import agent configuration from JSON file",
831
+ )
263
832
  @output_flags()
264
833
  @click.pass_context
265
834
  def create(
266
835
  ctx,
267
836
  name,
268
837
  instruction,
838
+ model,
269
839
  tools,
270
840
  agents,
271
841
  timeout,
842
+ import_file,
272
843
  ):
273
- """Create a new agent."""
844
+ """Create a new agent.
845
+
846
+ Examples:
847
+ aip agents create --name "My Agent" --instruction "You are a helpful assistant"
848
+ aip agents create --import agent.json
849
+ """
274
850
  try:
275
851
  client = get_client(ctx)
276
- # Create agent (uses backend default model)
277
- agent = client.agents.create_agent(
278
- name=name,
279
- instruction=instruction,
280
- tools=list(tools),
281
- agents=list(agents),
282
- timeout=timeout,
852
+
853
+ # Handle import from file
854
+ if import_file:
855
+ import_data = _load_agent_from_file(Path(import_file))
856
+
857
+ # Convert export format to import-compatible format
858
+ import_data = _convert_export_to_import_format(import_data)
859
+
860
+ # Merge CLI args with imported data
861
+ cli_args = {
862
+ "name": name,
863
+ "instruction": instruction,
864
+ "model": model,
865
+ "tools": tools or (),
866
+ "agents": agents or (),
867
+ "timeout": timeout if timeout != DEFAULT_AGENT_RUN_TIMEOUT else None,
868
+ }
869
+
870
+ merged_data = _merge_import_with_cli_args(import_data, cli_args)
871
+
872
+ # Extract merged values
873
+ name = merged_data.get("name")
874
+ instruction = merged_data.get("instruction")
875
+ model = merged_data.get("model")
876
+ tools = tuple(merged_data.get("tools", ()))
877
+ agents = tuple(merged_data.get("agents", ()))
878
+ timeout = merged_data.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
879
+
880
+ # Validate required fields
881
+ if not name:
882
+ raise click.ClickException("Agent name is required (--name or --import)")
883
+ if not instruction:
884
+ raise click.ClickException(
885
+ "Agent instruction is required (--instruction or --import)"
886
+ )
887
+
888
+ # Resolve tool and agent references: accept names or IDs
889
+ resolved_tools = _resolve_resources_by_name(
890
+ client, tools, "tool", client.find_tools, "Tool"
891
+ )
892
+ resolved_agents = _resolve_resources_by_name(
893
+ client, agents, "agent", client.find_agents, "Agent"
283
894
  )
284
895
 
896
+ # Create agent with comprehensive attribute support
897
+ create_kwargs = {
898
+ "name": name,
899
+ "instruction": instruction,
900
+ "tools": resolved_tools or None,
901
+ "agents": resolved_agents or None,
902
+ "timeout": timeout,
903
+ }
904
+
905
+ # Add model if specified (prioritize CLI model over imported model)
906
+ if model:
907
+ create_kwargs["model"] = model
908
+ elif (
909
+ import_file
910
+ and "agent_config" in merged_data
911
+ and merged_data["agent_config"]
912
+ ):
913
+ # Use lm_name from agent_config for cloning
914
+ agent_config = merged_data["agent_config"]
915
+ if isinstance(agent_config, dict) and "lm_name" in agent_config:
916
+ create_kwargs["model"] = agent_config["lm_name"]
917
+
918
+ # If importing from file, include all other detected attributes
919
+ if import_file:
920
+ # Add all other attributes from import data (excluding already handled ones and system-only fields)
921
+ excluded_fields = {
922
+ "name",
923
+ "instruction",
924
+ "model",
925
+ "tools",
926
+ "agents",
927
+ "timeout",
928
+ # System-only fields that shouldn't be passed to create_agent
929
+ "id",
930
+ "created_at",
931
+ "updated_at",
932
+ "agent_config",
933
+ "language_model_id",
934
+ "type",
935
+ "framework",
936
+ "version",
937
+ "tool_configs",
938
+ "mcps",
939
+ "a2a_profile",
940
+ }
941
+ for key, value in merged_data.items():
942
+ if key not in excluded_fields and value is not None:
943
+ create_kwargs[key] = value
944
+
945
+ agent = client.agents.create_agent(**create_kwargs)
946
+
285
947
  if ctx.obj.get("view") == "json":
286
948
  click.echo(json.dumps(agent.model_dump(), indent=2))
287
949
  else:
288
950
  # Rich output
289
- lm = getattr(agent, "model", None)
290
- if not lm:
291
- cfg = getattr(agent, "agent_config", {}) or {}
292
- lm = (
293
- cfg.get("lm_name")
294
- or cfg.get("model")
295
- or "gpt-4.1 (backend default)"
296
- )
951
+ _display_agent_creation_success(agent, model)
297
952
 
298
- panel = Panel(
299
- f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
300
- f"ID: {agent.id}\n"
301
- f"Model: {lm}\n"
302
- f"Type: {getattr(agent, 'type', 'config')}\n"
303
- f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
304
- f"Version: {getattr(agent, 'version', '1.0')}",
305
- title="🤖 Agent Created",
306
- border_style="green",
307
- )
308
- console.print(panel)
953
+ # Show run suggestions (only in rich mode, not JSON)
954
+ if ctx.obj.get("view") != "json":
955
+ _display_run_suggestions(agent)
309
956
 
310
957
  except Exception as e:
311
958
  if ctx.obj.get("view") == "json":
312
959
  click.echo(json.dumps({"error": str(e)}, indent=2))
313
960
  else:
314
- console.print(f"[red]Error creating agent: {e}[/red]")
961
+ console.print(Text(f"[red]Error creating agent: {e}[/red]"))
315
962
  raise click.ClickException(str(e))
316
963
 
317
964
 
@@ -322,10 +969,21 @@ def create(
322
969
  @click.option("--tools", multiple=True, help="New tool names or IDs")
323
970
  @click.option("--agents", multiple=True, help="New sub-agent names")
324
971
  @click.option("--timeout", type=int, help="New timeout value")
972
+ @click.option(
973
+ "--import",
974
+ "import_file",
975
+ type=click.Path(exists=True, dir_okay=False),
976
+ help="Import agent configuration from JSON file",
977
+ )
325
978
  @output_flags()
326
979
  @click.pass_context
327
- def update(ctx, agent_id, name, instruction, tools, agents, timeout):
328
- """Update an existing agent."""
980
+ def update(ctx, agent_id, name, instruction, tools, agents, timeout, import_file):
981
+ """Update an existing agent.
982
+
983
+ Examples:
984
+ aip agents update my-agent --instruction "New instruction"
985
+ aip agents update my-agent --import agent.json
986
+ """
329
987
  try:
330
988
  client = get_client(ctx)
331
989
 
@@ -335,7 +993,32 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
335
993
  except Exception as e:
336
994
  raise click.ClickException(f"Agent with ID '{agent_id}' not found: {e}")
337
995
 
338
- # Build update data
996
+ # Handle import from file
997
+ if import_file:
998
+ import_data = _load_agent_from_file(Path(import_file))
999
+
1000
+ # Convert export format to import-compatible format
1001
+ import_data = _convert_export_to_import_format(import_data)
1002
+
1003
+ # Merge CLI args with imported data
1004
+ cli_args = {
1005
+ "name": name,
1006
+ "instruction": instruction,
1007
+ "tools": tools or (),
1008
+ "agents": agents or (),
1009
+ "timeout": timeout,
1010
+ }
1011
+
1012
+ merged_data = _merge_import_with_cli_args(import_data, cli_args)
1013
+
1014
+ # Extract merged values
1015
+ name = merged_data.get("name")
1016
+ instruction = merged_data.get("instruction")
1017
+ tools = tuple(merged_data.get("tools", ()))
1018
+ agents = tuple(merged_data.get("agents", ()))
1019
+ timeout = merged_data.get("timeout")
1020
+
1021
+ # Build update data with comprehensive attribute support
339
1022
  update_data = {}
340
1023
  if name is not None:
341
1024
  update_data["name"] = name
@@ -348,24 +1031,51 @@ def update(ctx, agent_id, name, instruction, tools, agents, timeout):
348
1031
  if timeout is not None:
349
1032
  update_data["timeout"] = timeout
350
1033
 
1034
+ # If importing from file, include all other detected attributes
1035
+ if import_file:
1036
+ # Add all other attributes from import data (excluding already handled ones and system-only fields)
1037
+ excluded_fields = {
1038
+ "name",
1039
+ "instruction",
1040
+ "tools",
1041
+ "agents",
1042
+ "timeout",
1043
+ # System-only fields that shouldn't be passed to update_agent
1044
+ "id",
1045
+ "created_at",
1046
+ "updated_at",
1047
+ "agent_config",
1048
+ "type",
1049
+ "framework",
1050
+ "version",
1051
+ "tool_configs",
1052
+ "mcps",
1053
+ "a2a_profile",
1054
+ }
1055
+ for key, value in merged_data.items():
1056
+ if key not in excluded_fields and value is not None:
1057
+ update_data[key] = value
1058
+
351
1059
  if not update_data:
352
1060
  raise click.ClickException("No update fields specified")
353
1061
 
354
1062
  # Update agent
355
- updated_agent = client.agents.update_agent(agent.id, update_data)
1063
+ updated_agent = client.agents.update_agent(agent.id, **update_data)
356
1064
 
357
1065
  if ctx.obj.get("view") == "json":
358
1066
  click.echo(json.dumps(updated_agent.model_dump(), indent=2))
359
1067
  else:
360
- console.print(
361
- f"[green]✅ Agent '{updated_agent.name}' updated successfully[/green]"
362
- )
1068
+ _display_agent_update_success(updated_agent)
1069
+
1070
+ # Show run suggestions (only in rich mode, not JSON)
1071
+ if ctx.obj.get("view") != "json":
1072
+ _display_run_suggestions(updated_agent)
363
1073
 
364
1074
  except Exception as e:
365
1075
  if ctx.obj.get("view") == "json":
366
1076
  click.echo(json.dumps({"error": str(e)}, indent=2))
367
1077
  else:
368
- console.print(f"[red]Error updating agent: {e}[/red]")
1078
+ console.print(Text(f"[red]Error updating agent: {e}[/red]"))
369
1079
  raise click.ClickException(str(e))
370
1080
 
371
1081
 
@@ -390,7 +1100,7 @@ def delete(ctx, agent_id, yes):
390
1100
  f"Are you sure you want to delete agent '{agent.name}'?"
391
1101
  ):
392
1102
  if ctx.obj.get("view") != "json":
393
- console.print("Deletion cancelled.")
1103
+ console.print(Text("Deletion cancelled."))
394
1104
  return
395
1105
 
396
1106
  client.agents.delete_agent(agent.id)
@@ -404,12 +1114,12 @@ def delete(ctx, agent_id, yes):
404
1114
  )
405
1115
  else:
406
1116
  console.print(
407
- f"[green]✅ Agent '{agent.name}' deleted successfully[/green]"
1117
+ Text(f"[green]✅ Agent '{agent.name}' deleted successfully[/green]")
408
1118
  )
409
1119
 
410
1120
  except Exception as e:
411
1121
  if ctx.obj.get("view") == "json":
412
1122
  click.echo(json.dumps({"error": str(e)}, indent=2))
413
1123
  else:
414
- console.print(f"[red]Error deleting agent: {e}[/red]")
1124
+ console.print(Text(f"[red]Error deleting agent: {e}[/red]"))
415
1125
  raise click.ClickException(str(e))