glaip-sdk 0.0.20__py3-none-any.whl → 0.1.3__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 (66) hide show
  1. glaip_sdk/_version.py +1 -3
  2. glaip_sdk/branding.py +2 -6
  3. glaip_sdk/cli/agent_config.py +2 -6
  4. glaip_sdk/cli/auth.py +11 -30
  5. glaip_sdk/cli/commands/agents.py +64 -107
  6. glaip_sdk/cli/commands/configure.py +12 -36
  7. glaip_sdk/cli/commands/mcps.py +25 -63
  8. glaip_sdk/cli/commands/models.py +2 -4
  9. glaip_sdk/cli/commands/tools.py +22 -35
  10. glaip_sdk/cli/commands/update.py +3 -8
  11. glaip_sdk/cli/config.py +1 -3
  12. glaip_sdk/cli/display.py +10 -13
  13. glaip_sdk/cli/io.py +8 -14
  14. glaip_sdk/cli/main.py +10 -30
  15. glaip_sdk/cli/mcp_validators.py +5 -15
  16. glaip_sdk/cli/pager.py +3 -9
  17. glaip_sdk/cli/parsers/json_input.py +11 -22
  18. glaip_sdk/cli/resolution.py +3 -9
  19. glaip_sdk/cli/rich_helpers.py +1 -3
  20. glaip_sdk/cli/slash/agent_session.py +5 -10
  21. glaip_sdk/cli/slash/prompt.py +3 -10
  22. glaip_sdk/cli/slash/session.py +46 -98
  23. glaip_sdk/cli/transcript/cache.py +6 -19
  24. glaip_sdk/cli/transcript/capture.py +45 -20
  25. glaip_sdk/cli/transcript/launcher.py +1 -3
  26. glaip_sdk/cli/transcript/viewer.py +224 -47
  27. glaip_sdk/cli/update_notifier.py +165 -21
  28. glaip_sdk/cli/utils.py +33 -91
  29. glaip_sdk/cli/validators.py +11 -12
  30. glaip_sdk/client/_agent_payloads.py +10 -30
  31. glaip_sdk/client/agents.py +33 -63
  32. glaip_sdk/client/base.py +77 -35
  33. glaip_sdk/client/mcps.py +1 -3
  34. glaip_sdk/client/run_rendering.py +121 -26
  35. glaip_sdk/client/tools.py +8 -24
  36. glaip_sdk/client/validators.py +20 -48
  37. glaip_sdk/exceptions.py +1 -3
  38. glaip_sdk/icons.py +9 -3
  39. glaip_sdk/models.py +14 -33
  40. glaip_sdk/payload_schemas/agent.py +1 -3
  41. glaip_sdk/utils/agent_config.py +4 -14
  42. glaip_sdk/utils/client_utils.py +7 -21
  43. glaip_sdk/utils/display.py +2 -6
  44. glaip_sdk/utils/general.py +1 -3
  45. glaip_sdk/utils/import_export.py +3 -9
  46. glaip_sdk/utils/rendering/formatting.py +52 -12
  47. glaip_sdk/utils/rendering/models.py +17 -8
  48. glaip_sdk/utils/rendering/renderer/__init__.py +1 -5
  49. glaip_sdk/utils/rendering/renderer/base.py +1181 -328
  50. glaip_sdk/utils/rendering/renderer/config.py +4 -10
  51. glaip_sdk/utils/rendering/renderer/debug.py +4 -14
  52. glaip_sdk/utils/rendering/renderer/panels.py +1 -3
  53. glaip_sdk/utils/rendering/renderer/progress.py +3 -11
  54. glaip_sdk/utils/rendering/renderer/stream.py +9 -42
  55. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  56. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  57. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  58. glaip_sdk/utils/rendering/steps.py +899 -25
  59. glaip_sdk/utils/resource_refs.py +4 -13
  60. glaip_sdk/utils/serialization.py +14 -46
  61. glaip_sdk/utils/validation.py +4 -4
  62. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/METADATA +12 -1
  63. glaip_sdk-0.1.3.dist-info/RECORD +83 -0
  64. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  65. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/WHEEL +0 -0
  66. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.3.dist-info}/entry_points.txt +0 -0
@@ -90,9 +90,7 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
90
90
  return result if result else None
91
91
 
92
92
 
93
- def create_model_instances(
94
- data: list[dict] | None, model_class: type, client: Any
95
- ) -> list[Any]:
93
+ def create_model_instances(data: list[dict] | None, model_class: type, client: Any) -> list[Any]:
96
94
  """Create model instances from API data with client association.
97
95
 
98
96
  This is a common pattern used across different clients (agents, tools, mcps)
@@ -112,9 +110,7 @@ def create_model_instances(
112
110
  return [model_class(**item_data)._set_client(client) for item_data in data]
113
111
 
114
112
 
115
- def find_by_name(
116
- items: list[Any], name: str, case_sensitive: bool = False
117
- ) -> list[Any]:
113
+ def find_by_name(items: list[Any], name: str, case_sensitive: bool = False) -> list[Any]:
118
114
  """Filter items by name with optional case sensitivity.
119
115
 
120
116
  This is a common pattern used across different clients for client-side
@@ -234,9 +230,7 @@ def _handle_streaming_error(
234
230
  if isinstance(e, httpx.ReadTimeout):
235
231
  logger.error(f"Read timeout during streaming: {e}")
236
232
  logger.error("This usually indicates the backend is taking too long to respond")
237
- logger.error(
238
- "Consider increasing the timeout value or checking backend performance"
239
- )
233
+ logger.error("Consider increasing the timeout value or checking backend performance")
240
234
  raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
241
235
 
242
236
  elif isinstance(e, httpx.TimeoutException):
@@ -275,9 +269,7 @@ def _yield_event_data(event_data: dict[str, Any] | None) -> Iterator[dict[str, A
275
269
  yield event_data
276
270
 
277
271
 
278
- def _flush_remaining_buffer(
279
- buf: list[str], event_type: str | None, event_id: str | None
280
- ) -> Iterator[dict[str, Any]]:
272
+ def _flush_remaining_buffer(buf: list[str], event_type: str | None, event_id: str | None) -> Iterator[dict[str, Any]]:
281
273
  """Flush any remaining data in buffer."""
282
274
  if buf:
283
275
  yield {
@@ -317,9 +309,7 @@ def iter_sse_events(
317
309
  if line is None:
318
310
  continue
319
311
 
320
- buf, event_type, event_id, event_data, completed = _process_sse_line(
321
- line, buf, event_type, event_id
322
- )
312
+ buf, event_type, event_id, event_data, completed = _process_sse_line(line, buf, event_type, event_id)
323
313
 
324
314
  yield from _yield_event_data(event_data)
325
315
  if completed:
@@ -385,9 +375,7 @@ def _create_form_data(message: str) -> dict[str, Any]:
385
375
  return {"input": message, "message": message, "stream": True}
386
376
 
387
377
 
388
- def _prepare_file_entry(
389
- item: str | BinaryIO, stack: ExitStack
390
- ) -> tuple[str, tuple[str, BinaryIO, str]]:
378
+ def _prepare_file_entry(item: str | BinaryIO, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
391
379
  """Prepare a single file entry for multipart data."""
392
380
  if isinstance(item, str):
393
381
  return _prepare_path_entry(item, stack)
@@ -395,9 +383,7 @@ def _prepare_file_entry(
395
383
  return _prepare_stream_entry(item)
396
384
 
397
385
 
398
- def _prepare_path_entry(
399
- path_str: str, stack: ExitStack
400
- ) -> tuple[str, tuple[str, BinaryIO, str]]:
386
+ def _prepare_path_entry(path_str: str, stack: ExitStack) -> tuple[str, tuple[str, BinaryIO, str]]:
401
387
  """Prepare a file path entry."""
402
388
  file_path = Path(path_str)
403
389
  if not file_path.exists():
@@ -109,9 +109,7 @@ def print_agent_updated(agent: Any) -> None:
109
109
  """
110
110
  if RICH_AVAILABLE:
111
111
  console = _create_console()
112
- console.print(
113
- f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]"
114
- )
112
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]")
115
113
  else:
116
114
  print(f"✅ Agent '{agent.name}' updated successfully")
117
115
 
@@ -124,8 +122,6 @@ def print_agent_deleted(agent_id: str) -> None:
124
122
  """
125
123
  if RICH_AVAILABLE:
126
124
  console = _create_console()
127
- console.print(
128
- f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]"
129
- )
125
+ console.print(f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]")
130
126
  else:
131
127
  print(f"✅ Agent deleted successfully (ID: {agent_id})")
@@ -76,9 +76,7 @@ def format_datetime(dt: datetime | str | None) -> str:
76
76
  return str(dt)
77
77
 
78
78
 
79
- def progress_bar(
80
- iterable: Iterable[Any], description: str = "Processing"
81
- ) -> Iterator[Any]:
79
+ def progress_bar(iterable: Iterable[Any], description: str = "Processing") -> Iterator[Any]:
82
80
  """Simple progress bar using click.
83
81
 
84
82
  Args:
@@ -71,14 +71,10 @@ def _get_default_array_fields() -> list[str]:
71
71
 
72
72
  def _should_use_cli_value(cli_value: Any) -> bool:
73
73
  """Check if CLI value should be used."""
74
- return cli_value is not None and (
75
- not isinstance(cli_value, list | tuple) or len(cli_value) > 0
76
- )
74
+ return cli_value is not None and (not isinstance(cli_value, (list, tuple)) or len(cli_value) > 0)
77
75
 
78
76
 
79
- def _handle_array_field_merge(
80
- key: str, cli_value: Any, import_data: dict[str, Any]
81
- ) -> Any:
77
+ def _handle_array_field_merge(key: str, cli_value: Any, import_data: dict[str, Any]) -> Any:
82
78
  """Handle merging of array fields."""
83
79
  import_value = import_data[key]
84
80
  if isinstance(import_value, list):
@@ -107,9 +103,7 @@ def _merge_cli_values_with_import(
107
103
  merged[key] = import_data[key]
108
104
 
109
105
 
110
- def _add_import_only_fields(
111
- merged: dict[str, Any], import_data: dict[str, Any]
112
- ) -> None:
106
+ def _add_import_only_fields(merged: dict[str, Any], import_data: dict[str, Any]) -> None:
113
107
  """Add fields that exist only in import data."""
114
108
  for key, import_value in import_data.items():
115
109
  if key not in merged:
@@ -12,7 +12,14 @@ import time
12
12
  from collections.abc import Callable
13
13
  from typing import Any
14
14
 
15
- from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
15
+ from glaip_sdk.icons import (
16
+ ICON_AGENT_STEP,
17
+ ICON_DELEGATE,
18
+ ICON_STATUS_FAILED,
19
+ ICON_STATUS_SUCCESS,
20
+ ICON_STATUS_WARNING,
21
+ ICON_TOOL_STEP,
22
+ )
16
23
 
17
24
  # Constants for argument formatting
18
25
  DEFAULT_ARGS_MAX_LEN = 100
@@ -37,9 +44,20 @@ SECRET_VALUE_PATTERNS = [
37
44
  re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
38
45
  ]
39
46
  SENSITIVE_PATTERNS = re.compile(
40
- r"password\s*[:=]\s*[^\s,}]+|secret\s*[:=]\s*[^\s,}]+|token\s*[:=]\s*[^\s,}]+|key\s*[:=]\s*[^\s,}]+|api_key\s*[:=]\s*[^\s,}]+|^password$|^secret$|^token$|^key$|^api_key$",
47
+ r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
41
48
  re.IGNORECASE,
42
49
  )
50
+ CONNECTOR_VERTICAL = "│ "
51
+ CONNECTOR_EMPTY = " "
52
+ CONNECTOR_BRANCH = "├─ "
53
+ CONNECTOR_LAST = "└─ "
54
+ ROOT_MARKER = ""
55
+ SECRET_MASK = "••••••"
56
+ STATUS_GLYPHS = {
57
+ "success": ICON_STATUS_SUCCESS,
58
+ "failed": ICON_STATUS_FAILED,
59
+ "warning": ICON_STATUS_WARNING,
60
+ }
43
61
 
44
62
 
45
63
  def _truncate_string(s: str, max_len: int) -> str:
@@ -53,7 +71,7 @@ def mask_secrets_in_string(text: str) -> str:
53
71
  """Mask sensitive information in a string."""
54
72
  result = text
55
73
  for pattern in SECRET_VALUE_PATTERNS:
56
- result = re.sub(pattern, "••••••", result)
74
+ result = re.sub(pattern, SECRET_MASK, result)
57
75
  return result
58
76
 
59
77
 
@@ -74,7 +92,7 @@ def _redact_dict_values(text: dict) -> dict:
74
92
  result = {}
75
93
  for key, value in text.items():
76
94
  if _is_sensitive_key(key):
77
- result[key] = "••••••"
95
+ result[key] = SECRET_MASK
78
96
  elif _should_recurse_redaction(value):
79
97
  result[key] = redact_sensitive(value)
80
98
  else:
@@ -92,11 +110,11 @@ def _redact_string_content(text: str) -> str:
92
110
  result = text
93
111
  # First mask secrets
94
112
  for pattern in SECRET_VALUE_PATTERNS:
95
- result = re.sub(pattern, "••••••", result)
113
+ result = re.sub(pattern, SECRET_MASK, result)
96
114
  # Then redact sensitive patterns
97
115
  result = re.sub(
98
116
  SENSITIVE_PATTERNS,
99
- lambda m: m.group(0).split("=")[0] + "=••••••",
117
+ lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
100
118
  result,
101
119
  )
102
120
  return result
@@ -105,15 +123,37 @@ def _redact_string_content(text: str) -> str:
105
123
  def _is_sensitive_key(key: str) -> bool:
106
124
  """Check if a key contains sensitive information."""
107
125
  key_lower = key.lower()
108
- return any(
109
- sensitive in key_lower
110
- for sensitive in ["password", "secret", "token", "key", "api_key"]
111
- )
126
+ return any(sensitive in key_lower for sensitive in ["password", "secret", "token", "key", "api_key"])
112
127
 
113
128
 
114
129
  def _should_recurse_redaction(value: Any) -> bool:
115
130
  """Check if a value should be recursively processed."""
116
- return isinstance(value, dict | list) or isinstance(value, str)
131
+ return isinstance(value, (dict, list)) or isinstance(value, str)
132
+
133
+
134
+ def glyph_for_status(icon_key: str | None) -> str | None:
135
+ """Return glyph representing a step status icon key."""
136
+ if not icon_key:
137
+ return None
138
+ return STATUS_GLYPHS.get(icon_key)
139
+
140
+
141
+ def normalise_display_label(label: str | None) -> str:
142
+ """Return a user facing label or the Unknown fallback."""
143
+ label = (label or "").strip()
144
+ return label or "Unknown step detail"
145
+
146
+
147
+ def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
148
+ """Build connector prefix for a tree line based on ancestry state."""
149
+ if not branch_state:
150
+ return ROOT_MARKER
151
+
152
+ parts: list[str] = []
153
+ for ancestor_is_last in branch_state[:-1]:
154
+ parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
155
+ parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
156
+ return "".join(parts)
117
157
 
118
158
 
119
159
  def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
@@ -132,7 +172,7 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
132
172
  try:
133
173
  args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
134
174
  return _truncate_string(args_str, max_len)
135
- except (TypeError, ValueError, Exception):
175
+ except Exception:
136
176
  # Fallback to string representation if JSON serialization fails
137
177
  args_str = str(masked_args)
138
178
  return _truncate_string(args_str, max_len)
@@ -30,19 +30,32 @@ class Step:
30
30
  context_id: str | None = None
31
31
  started_at: float = field(default_factory=monotonic)
32
32
  duration_ms: int | None = None
33
-
34
- def finish(self, duration_raw: float | None) -> None:
33
+ duration_source: str | None = None
34
+ display_label: str | None = None
35
+ status_icon: str | None = None
36
+ failure_reason: str | None = None
37
+ branch_failed: bool = False
38
+ is_parallel: bool = False
39
+ server_started_at: float | None = None
40
+ server_finished_at: float | None = None
41
+ duration_unknown: bool = False
42
+
43
+ def finish(self, duration_raw: float | None, *, source: str | None = None) -> None:
35
44
  """Mark the step as finished and calculate duration.
36
45
 
37
46
  Args:
38
47
  duration_raw: Raw duration in seconds, or None to calculate from started_at
48
+ source: Optional duration source tag
39
49
  """
40
- if isinstance(duration_raw, int | float) and duration_raw > 0:
50
+ self.duration_unknown = False
51
+ if isinstance(duration_raw, (int, float)) and duration_raw > 0:
41
52
  # Use provided duration if it's a positive number (even if very small)
42
53
  self.duration_ms = round(float(duration_raw) * 1000)
54
+ self.duration_source = source or self.duration_source or "provided"
43
55
  else:
44
56
  # Calculate from started_at if duration_raw is None, negative, or zero
45
57
  self.duration_ms = int((monotonic() - self.started_at) * 1000)
58
+ self.duration_source = source or self.duration_source or "monotonic"
46
59
  self.status = "finished"
47
60
 
48
61
 
@@ -68,8 +81,4 @@ class RunStats:
68
81
  Returns:
69
82
  Duration in seconds if run is finished, None otherwise
70
83
  """
71
- return (
72
- None
73
- if self.finished_at is None
74
- else round(self.finished_at - self.started_at, 2)
75
- )
84
+ return None if self.finished_at is None else round(self.finished_at - self.started_at, 2)
@@ -35,12 +35,9 @@ def make_silent_renderer() -> RichStreamRenderer:
35
35
  cfg = RendererConfig(
36
36
  live=False,
37
37
  persist_live=False,
38
- show_delegate_tool_panels=False,
39
38
  render_thinking=False,
40
39
  )
41
- return RichStreamRenderer(
42
- console=Console(file=io.StringIO(), force_terminal=False), cfg=cfg
43
- )
40
+ return RichStreamRenderer(console=Console(file=io.StringIO(), force_terminal=False), cfg=cfg)
44
41
 
45
42
 
46
43
  def make_minimal_renderer() -> RichStreamRenderer:
@@ -51,7 +48,6 @@ def make_minimal_renderer() -> RichStreamRenderer:
51
48
  cfg = RendererConfig(
52
49
  live=False,
53
50
  persist_live=False,
54
- show_delegate_tool_panels=False,
55
51
  render_thinking=False,
56
52
  )
57
53
  return RichStreamRenderer(console=Console(), cfg=cfg)