glaip-sdk 0.0.20__py3-none-any.whl → 0.1.1__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 (65) 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 +4 -12
  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 +6 -20
  25. glaip_sdk/cli/transcript/launcher.py +1 -3
  26. glaip_sdk/cli/transcript/viewer.py +187 -46
  27. glaip_sdk/cli/update_notifier.py +165 -21
  28. glaip_sdk/cli/utils.py +33 -85
  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 +6 -22
  33. glaip_sdk/client/mcps.py +1 -3
  34. glaip_sdk/client/run_rendering.py +121 -24
  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 +1107 -320
  50. glaip_sdk/utils/rendering/renderer/config.py +3 -5
  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 +10 -22
  55. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  56. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  57. glaip_sdk/utils/rendering/steps.py +899 -25
  58. glaip_sdk/utils/resource_refs.py +4 -13
  59. glaip_sdk/utils/serialization.py +14 -46
  60. glaip_sdk/utils/validation.py +4 -4
  61. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/METADATA +12 -1
  62. glaip_sdk-0.1.1.dist-info/RECORD +82 -0
  63. glaip_sdk-0.0.20.dist-info/RECORD +0 -80
  64. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/WHEEL +0 -0
  65. {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.1.dist-info}/entry_points.txt +0 -0
@@ -348,9 +348,7 @@ class AgentCreateRequest:
348
348
  payload["tool_configs"] = _copy_structure(self.tool_configs)
349
349
 
350
350
  effective_agent_config = _sanitize_agent_config(self.agent_config)
351
- effective_agent_config = _merge_execution_timeout(
352
- effective_agent_config, self.timeout
353
- )
351
+ effective_agent_config = _merge_execution_timeout(effective_agent_config, self.timeout)
354
352
  if effective_agent_config:
355
353
  payload["agent_config"] = effective_agent_config
356
354
 
@@ -407,31 +405,19 @@ __all__ = [
407
405
  ]
408
406
 
409
407
 
410
- def _build_base_update_payload(
411
- request: AgentUpdateRequest, current_agent: Any
412
- ) -> dict[str, Any]:
408
+ def _build_base_update_payload(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
413
409
  return {
414
- "name": request.name.strip()
415
- if request.name is not None
416
- else getattr(current_agent, "name", None),
410
+ "name": request.name.strip() if request.name is not None else getattr(current_agent, "name", None),
417
411
  "instruction": request.instruction.strip()
418
412
  if request.instruction is not None
419
413
  else getattr(current_agent, "instruction", None),
420
- "type": request.agent_type
421
- or getattr(current_agent, "type", None)
422
- or DEFAULT_AGENT_TYPE,
423
- "framework": request.framework
424
- or getattr(current_agent, "framework", None)
425
- or DEFAULT_AGENT_FRAMEWORK,
426
- "version": request.version
427
- or getattr(current_agent, "version", None)
428
- or DEFAULT_AGENT_VERSION,
414
+ "type": request.agent_type or getattr(current_agent, "type", None) or DEFAULT_AGENT_TYPE,
415
+ "framework": request.framework or getattr(current_agent, "framework", None) or DEFAULT_AGENT_FRAMEWORK,
416
+ "version": request.version or getattr(current_agent, "version", None) or DEFAULT_AGENT_VERSION,
429
417
  }
430
418
 
431
419
 
432
- def _resolve_update_language_model_fields(
433
- request: AgentUpdateRequest, current_agent: Any
434
- ) -> dict[str, Any]:
420
+ def _resolve_update_language_model_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
435
421
  fields = resolve_language_model_fields(
436
422
  model=request.model,
437
423
  language_model_id=request.language_model_id,
@@ -443,9 +429,7 @@ def _resolve_update_language_model_fields(
443
429
  return fields
444
430
 
445
431
 
446
- def _collect_optional_update_fields(
447
- request: AgentUpdateRequest, current_agent: Any
448
- ) -> dict[str, Any]:
432
+ def _collect_optional_update_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
449
433
  result: dict[str, Any] = {}
450
434
 
451
435
  for field_name, value in (
@@ -483,9 +467,7 @@ def _collect_optional_update_fields(
483
467
  return result
484
468
 
485
469
 
486
- def _collect_relationship_fields(
487
- request: AgentUpdateRequest, current_agent: Any
488
- ) -> dict[str, Any]:
470
+ def _collect_relationship_fields(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any]:
489
471
  return {
490
472
  "tools": _resolve_relation_ids(request.tools, current_agent, "tools"),
491
473
  "agents": _resolve_relation_ids(request.agents, current_agent, "agents"),
@@ -493,9 +475,7 @@ def _collect_relationship_fields(
493
475
  }
494
476
 
495
477
 
496
- def _resolve_agent_config_update(
497
- request: AgentUpdateRequest, current_agent: Any
498
- ) -> dict[str, Any] | None:
478
+ def _resolve_agent_config_update(request: AgentUpdateRequest, current_agent: Any) -> dict[str, Any] | None:
499
479
  effective_agent_config = _sanitize_agent_config(request.agent_config)
500
480
  if effective_agent_config is not None:
501
481
  return effective_agent_config
@@ -34,7 +34,7 @@ from glaip_sdk.config.constants import (
34
34
  DEFAULT_AGENT_VERSION,
35
35
  DEFAULT_MODEL,
36
36
  )
37
- from glaip_sdk.exceptions import NotFoundError
37
+ from glaip_sdk.exceptions import NotFoundError, ValidationError
38
38
  from glaip_sdk.models import Agent
39
39
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
40
40
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
@@ -97,9 +97,7 @@ def _merge_override_maps(
97
97
  for key, value in source.items():
98
98
  if value is None:
99
99
  continue
100
- merged[key] = (
101
- _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
102
- )
100
+ merged[key] = _normalise_sequence(value) if key in _MERGED_SEQUENCE_FIELDS else value
103
101
  return merged
104
102
 
105
103
 
@@ -134,9 +132,7 @@ def _prepare_agent_metadata(value: Any) -> dict[str, Any]:
134
132
  return prepared
135
133
 
136
134
 
137
- def _load_agent_file_payload(
138
- file_path: Path, *, model_override: str | None
139
- ) -> dict[str, Any]:
135
+ def _load_agent_file_payload(file_path: Path, *, model_override: str | None) -> dict[str, Any]:
140
136
  """Load agent configuration from disk and normalise legacy fields."""
141
137
  if not file_path.exists():
142
138
  raise FileNotFoundError(f"Agent configuration file not found: {file_path}")
@@ -168,9 +164,7 @@ def _prepare_import_payload(
168
164
  raw_definition = load_resource_from_file(file_path)
169
165
  original_refs = _extract_original_refs(raw_definition)
170
166
 
171
- base_payload = _load_agent_file_payload(
172
- file_path, model_override=overrides_dict.get("model")
173
- )
167
+ base_payload = _load_agent_file_payload(file_path, model_override=overrides_dict.get("model"))
174
168
 
175
169
  cli_args = _build_cli_args(overrides_dict)
176
170
 
@@ -223,11 +217,7 @@ def _build_cli_args(overrides_dict: dict) -> dict[str, Any]:
223
217
 
224
218
  def _build_additional_args(overrides_dict: dict, cli_args: dict) -> dict[str, Any]:
225
219
  """Build additional args not already in CLI args."""
226
- return {
227
- key: value
228
- for key, value in overrides_dict.items()
229
- if value is not None and key not in cli_args
230
- }
220
+ return {key: value for key, value in overrides_dict.items() if value is not None and key not in cli_args}
231
221
 
232
222
 
233
223
  def _remove_model_fields_if_needed(merged: dict, overrides_dict: dict) -> None:
@@ -278,9 +268,7 @@ class AgentClient(BaseClient):
278
268
  """
279
269
  if query is not None and kwargs:
280
270
  # Both query object and individual parameters provided
281
- raise ValueError(
282
- "Provide either `query` or individual filter arguments, not both."
283
- )
271
+ raise ValueError("Provide either `query` or individual filter arguments, not both.")
284
272
 
285
273
  if query is None:
286
274
  # Create query from individual parameters for backward compatibility
@@ -339,7 +327,19 @@ class AgentClient(BaseClient):
339
327
 
340
328
  def get_agent_by_id(self, agent_id: str) -> Agent:
341
329
  """Get agent by ID."""
342
- data = self._request("GET", f"/agents/{agent_id}")
330
+ try:
331
+ data = self._request("GET", f"/agents/{agent_id}")
332
+ except ValidationError as exc:
333
+ if exc.status_code == 422:
334
+ message = f"Agent '{agent_id}' not found"
335
+ raise NotFoundError(
336
+ message,
337
+ status_code=404,
338
+ error_type=exc.error_type,
339
+ payload=exc.payload,
340
+ request_id=exc.request_id,
341
+ ) from exc
342
+ raise
343
343
 
344
344
  if isinstance(data, str):
345
345
  # Some backends may respond with plain text for missing agents.
@@ -372,9 +372,7 @@ class AgentClient(BaseClient):
372
372
  self._renderer_manager = manager
373
373
  return manager
374
374
 
375
- def _create_renderer(
376
- self, renderer: RichStreamRenderer | str | None, **kwargs: Any
377
- ) -> RichStreamRenderer:
375
+ def _create_renderer(self, renderer: RichStreamRenderer | str | None, **kwargs: Any) -> RichStreamRenderer:
378
376
  manager = self._get_renderer_manager()
379
377
  verbose = kwargs.get("verbose", False)
380
378
  if isinstance(renderer, RichStreamRenderer) or hasattr(renderer, "on_start"):
@@ -506,9 +504,7 @@ class AgentClient(BaseClient):
506
504
  return entry_id
507
505
 
508
506
  if entry_name:
509
- resolved, success = self._resolve_resource_by_name(
510
- find_by_name, entry_name, singular, plural
511
- )
507
+ resolved, success = self._resolve_resource_by_name(find_by_name, entry_name, singular, plural)
512
508
  return resolved if success else entry_name
513
509
 
514
510
  raise ValueError(f"{singular} references must include a valid ID or name.")
@@ -523,9 +519,7 @@ class AgentClient(BaseClient):
523
519
  return str(entry)
524
520
 
525
521
  @staticmethod
526
- def _validate_resource_id(
527
- fetch_by_id: Callable[[str], Any], candidate_id: str | None
528
- ) -> str | None:
522
+ def _validate_resource_id(fetch_by_id: Callable[[str], Any], candidate_id: str | None) -> str | None:
529
523
  if not candidate_id:
530
524
  return None
531
525
  try:
@@ -547,21 +541,13 @@ class AgentClient(BaseClient):
547
541
  return entry_name, False
548
542
 
549
543
  if not matches:
550
- raise ValueError(
551
- f"{singular} '{entry_name}' not found in current workspace."
552
- )
544
+ raise ValueError(f"{singular} '{entry_name}' not found in current workspace.")
553
545
  if len(matches) > 1:
554
- exact = [
555
- m
556
- for m in matches
557
- if getattr(m, "name", "").lower() == entry_name.lower()
558
- ]
546
+ exact = [m for m in matches if getattr(m, "name", "").lower() == entry_name.lower()]
559
547
  if len(exact) == 1:
560
548
  matches = exact
561
549
  else:
562
- raise ValueError(
563
- f"Multiple {plural} named '{entry_name}'. Please disambiguate."
564
- )
550
+ raise ValueError(f"Multiple {plural} named '{entry_name}'. Please disambiguate.")
565
551
  return str(matches[0].id), True
566
552
 
567
553
  def _resolve_tool_ids(
@@ -610,9 +596,7 @@ class AgentClient(BaseClient):
610
596
 
611
597
  def _create_agent_from_payload(self, payload: Mapping[str, Any]) -> "Agent":
612
598
  """Create an agent using a fully prepared payload mapping."""
613
- known, extras = _split_known_and_extra(
614
- payload, AgentCreateRequest.__dataclass_fields__
615
- )
599
+ known, extras = _split_known_and_extra(payload, AgentCreateRequest.__dataclass_fields__)
616
600
 
617
601
  name = known.pop("name", None)
618
602
  instruction = known.pop("instruction", None)
@@ -721,9 +705,7 @@ class AgentClient(BaseClient):
721
705
  overrides = _merge_override_maps(base_overrides, kwargs)
722
706
 
723
707
  if file is not None:
724
- payload = _prepare_import_payload(
725
- Path(file).expanduser(), overrides, drop_model_fields=True
726
- )
708
+ payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
727
709
  if overrides.get("model") is None:
728
710
  payload.pop("model", None)
729
711
  else:
@@ -746,9 +728,7 @@ class AgentClient(BaseClient):
746
728
  payload: Mapping[str, Any],
747
729
  ) -> "Agent":
748
730
  """Update an agent using a prepared payload mapping."""
749
- known, extras = _split_known_and_extra(
750
- payload, AgentUpdateRequest.__dataclass_fields__
751
- )
731
+ known, extras = _split_known_and_extra(payload, AgentUpdateRequest.__dataclass_fields__)
752
732
  _normalise_sequence_fields(known)
753
733
 
754
734
  tool_refs = extras.pop("_tool_refs", None)
@@ -818,9 +798,7 @@ class AgentClient(BaseClient):
818
798
  overrides = _merge_override_maps(base_overrides, kwargs)
819
799
 
820
800
  if file is not None:
821
- payload = _prepare_import_payload(
822
- Path(file).expanduser(), overrides, drop_model_fields=True
823
- )
801
+ payload = _prepare_import_payload(Path(file).expanduser(), overrides, drop_model_fields=True)
824
802
  else:
825
803
  payload = overrides
826
804
 
@@ -882,9 +860,7 @@ class AgentClient(BaseClient):
882
860
  payload["tty"] = True
883
861
  return payload, None, None, headers, None
884
862
 
885
- def _get_timeout_values(
886
- self, timeout: float | None, **kwargs: Any
887
- ) -> tuple[float, float]:
863
+ def _get_timeout_values(self, timeout: float | None, **kwargs: Any) -> tuple[float, float]:
888
864
  """Get request timeout and execution timeout values.
889
865
 
890
866
  Args:
@@ -1009,9 +985,7 @@ class AgentClient(BaseClient):
1009
985
  headers = {"Accept": SSE_CONTENT_TYPE}
1010
986
  return payload, None, None, headers
1011
987
 
1012
- def _create_async_client_config(
1013
- self, timeout: float | None, headers: dict | None
1014
- ) -> dict:
988
+ def _create_async_client_config(self, timeout: float | None, headers: dict | None) -> dict:
1015
989
  """Create async client configuration with proper headers and timeout."""
1016
990
  config = self._build_async_client(timeout or self.timeout)
1017
991
  if headers:
@@ -1040,9 +1014,7 @@ class AgentClient(BaseClient):
1040
1014
  ) as stream_response:
1041
1015
  stream_response.raise_for_status()
1042
1016
 
1043
- async for event in aiter_sse_events(
1044
- stream_response, timeout_seconds, agent_name
1045
- ):
1017
+ async for event in aiter_sse_events(stream_response, timeout_seconds, agent_name):
1046
1018
  try:
1047
1019
  chunk = json.loads(event["data"])
1048
1020
  yield chunk
@@ -1077,9 +1049,7 @@ class AgentClient(BaseClient):
1077
1049
  Exception: For other unexpected errors
1078
1050
  """
1079
1051
  # Prepare request data
1080
- payload, data_payload, files_payload, headers = self._prepare_request_data(
1081
- message, files, **kwargs
1082
- )
1052
+ payload, data_payload, files_payload, headers = self._prepare_request_data(message, files, **kwargs)
1083
1053
 
1084
1054
  # Create async client configuration
1085
1055
  async_client_config = self._create_async_client_config(timeout, headers)
glaip_sdk/client/base.py CHANGED
@@ -151,12 +151,7 @@ class BaseClient:
151
151
  def timeout(self, value: float) -> None:
152
152
  """Set timeout and rebuild client."""
153
153
  self._timeout = value
154
- if (
155
- hasattr(self, "http_client")
156
- and self.http_client
157
- and not self._session_scoped
158
- and not self._parent_client
159
- ):
154
+ if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
160
155
  self.http_client.close()
161
156
  self.http_client = self._build_client(value)
162
157
 
@@ -246,18 +241,14 @@ class BaseClient:
246
241
  client_log.debug(f"Response status: {response.status_code}")
247
242
  return response
248
243
  except httpx.ConnectError as e:
249
- client_log.warning(
250
- f"Connection error on {method} {endpoint}, retrying once: {e}"
251
- )
244
+ client_log.warning(f"Connection error on {method} {endpoint}, retrying once: {e}")
252
245
  try:
253
246
  response = self.http_client.request(method, endpoint, **kwargs)
254
- client_log.debug(
255
- f"Retry successful, response status: {response.status_code}"
256
- )
247
+ client_log.debug(f"Retry successful, response status: {response.status_code}")
257
248
  return response
258
249
  except httpx.ConnectError:
259
250
  client_log.error(f"Retry failed for {method} {endpoint}: {e}")
260
- raise e
251
+ raise
261
252
 
262
253
  def _request(self, method: str, endpoint: str, **kwargs) -> Any:
263
254
  """Make HTTP request with error handling and unwrap success envelopes."""
@@ -381,9 +372,7 @@ class BaseClient:
381
372
  error_message = self._get_error_message(response)
382
373
  # Try to parse response content for payload
383
374
  parsed_content = self._parse_response_content(response)
384
- self._raise_api_error(
385
- response.status_code, error_message, payload=parsed_content
386
- )
375
+ self._raise_api_error(response.status_code, error_message, payload=parsed_content)
387
376
  return None # Won't be reached but helps with type checking
388
377
 
389
378
  parsed = self._parse_response_content(response)
@@ -435,12 +424,7 @@ class BaseClient:
435
424
 
436
425
  def close(self) -> None:
437
426
  """Close the HTTP client."""
438
- if (
439
- hasattr(self, "http_client")
440
- and self.http_client
441
- and not self._session_scoped
442
- and not self._parent_client
443
- ):
427
+ if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
444
428
  self.http_client.close()
445
429
 
446
430
  def __enter__(self) -> "BaseClient":
glaip_sdk/client/mcps.py CHANGED
@@ -179,9 +179,7 @@ class MCPClient(BaseClient):
179
179
  update_data = {
180
180
  "name": name if name is not None else current_mcp.name,
181
181
  "type": DEFAULT_MCP_TYPE, # Required by backend, MCPs are always server type
182
- "transport": kwargs.get(
183
- "transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)
184
- ),
182
+ "transport": kwargs.get("transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)),
185
183
  }
186
184
 
187
185
  # Handle description with proper None handling
@@ -23,6 +23,54 @@ from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
23
23
  from glaip_sdk.utils.rendering.renderer.config import RendererConfig
24
24
 
25
25
 
26
+ def _coerce_to_string(value: Any) -> str:
27
+ """Return a best-effort string representation for transcripts."""
28
+ try:
29
+ return str(value)
30
+ except Exception:
31
+ return f"{value}"
32
+
33
+
34
+ def _has_visible_text(value: Any) -> bool:
35
+ """Return True when the value is a non-empty string."""
36
+ return isinstance(value, str) and bool(value.strip())
37
+
38
+
39
+ def _update_state_transcript(state: Any, text_value: str) -> bool:
40
+ """Inject transcript text into renderer state if possible."""
41
+ if state is None:
42
+ return False
43
+
44
+ updated = False
45
+
46
+ if hasattr(state, "final_text") and not _has_visible_text(getattr(state, "final_text", "")):
47
+ try:
48
+ state.final_text = text_value
49
+ updated = True
50
+ except Exception:
51
+ pass
52
+
53
+ buffer = getattr(state, "buffer", None)
54
+ if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
55
+ buffer.append(text_value)
56
+ updated = True
57
+
58
+ return updated
59
+
60
+
61
+ def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
62
+ """Populate the renderer (or its state) with the supplied text."""
63
+ state = getattr(renderer, "state", None)
64
+ if _update_state_transcript(state, text_value):
65
+ return
66
+
67
+ if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
68
+ try:
69
+ renderer.final_text = text_value
70
+ except Exception:
71
+ pass
72
+
73
+
26
74
  class AgentRunRenderingManager:
27
75
  """Coordinate renderer creation and streaming event handling."""
28
76
 
@@ -79,7 +127,6 @@ class AgentRunRenderingManager:
79
127
  silent_config = RendererConfig(
80
128
  live=False,
81
129
  persist_live=False,
82
- show_delegate_tool_panels=False,
83
130
  render_thinking=False,
84
131
  )
85
132
  return RichStreamRenderer(
@@ -92,7 +139,6 @@ class AgentRunRenderingManager:
92
139
  minimal_config = RendererConfig(
93
140
  live=False,
94
141
  persist_live=False,
95
- show_delegate_tool_panels=False,
96
142
  render_thinking=False,
97
143
  )
98
144
  return RichStreamRenderer(
@@ -106,7 +152,6 @@ class AgentRunRenderingManager:
106
152
  theme="dark",
107
153
  style="debug",
108
154
  live=False,
109
- show_delegate_tool_panels=False,
110
155
  append_finished_snapshots=False,
111
156
  )
112
157
  return RichStreamRenderer(
@@ -139,17 +184,28 @@ class AgentRunRenderingManager:
139
184
 
140
185
  self._capture_request_id(stream_response, meta, renderer)
141
186
 
142
- for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
143
- if started_monotonic is None:
144
- started_monotonic = self._maybe_start_timer(event)
187
+ controller = getattr(renderer, "transcript_controller", None)
188
+ if controller and getattr(controller, "enabled", False):
189
+ controller.on_stream_start(renderer)
145
190
 
146
- final_text, stats_usage = self._process_single_event(
147
- event,
148
- renderer,
149
- final_text,
150
- stats_usage,
151
- meta,
152
- )
191
+ try:
192
+ for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
193
+ if started_monotonic is None:
194
+ started_monotonic = self._maybe_start_timer(event)
195
+
196
+ final_text, stats_usage = self._process_single_event(
197
+ event,
198
+ renderer,
199
+ final_text,
200
+ stats_usage,
201
+ meta,
202
+ )
203
+
204
+ if controller and getattr(controller, "enabled", False):
205
+ controller.poll(renderer)
206
+ finally:
207
+ if controller and getattr(controller, "enabled", False):
208
+ controller.on_stream_complete()
153
209
 
154
210
  finished_monotonic = monotonic()
155
211
  return final_text, stats_usage, started_monotonic, finished_monotonic
@@ -160,9 +216,7 @@ class AgentRunRenderingManager:
160
216
  meta: dict[str, Any],
161
217
  renderer: RichStreamRenderer,
162
218
  ) -> None:
163
- req_id = stream_response.headers.get(
164
- "x-request-id"
165
- ) or stream_response.headers.get("x-run-id")
219
+ req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
166
220
  if req_id:
167
221
  meta["run_id"] = req_id
168
222
  renderer.on_start(meta)
@@ -194,19 +248,50 @@ class AgentRunRenderingManager:
194
248
  kind = (ev.get("metadata") or {}).get("kind")
195
249
  renderer.on_event(ev)
196
250
 
251
+ handled = self._handle_metadata_kind(
252
+ kind,
253
+ ev,
254
+ final_text,
255
+ stats_usage,
256
+ meta,
257
+ renderer,
258
+ )
259
+ if handled is not None:
260
+ return handled
261
+
262
+ if ev.get("content"):
263
+ final_text = self._handle_content_event(ev, final_text)
264
+
265
+ return final_text, stats_usage
266
+
267
+ def _handle_metadata_kind(
268
+ self,
269
+ kind: str | None,
270
+ ev: dict[str, Any],
271
+ final_text: str,
272
+ stats_usage: dict[str, Any],
273
+ meta: dict[str, Any],
274
+ renderer: RichStreamRenderer,
275
+ ) -> tuple[str, dict[str, Any]] | None:
276
+ """Process well-known metadata kinds and return updated state."""
197
277
  if kind == "artifact":
198
278
  return final_text, stats_usage
199
279
 
200
- if kind == "final_response" and ev.get("content"):
201
- final_text = ev.get("content", "")
202
- elif ev.get("content"):
203
- final_text = self._handle_content_event(ev, final_text)
204
- elif kind == "usage":
280
+ if kind == "final_response":
281
+ content = ev.get("content")
282
+ if content:
283
+ return content, stats_usage
284
+ return final_text, stats_usage
285
+
286
+ if kind == "usage":
205
287
  stats_usage.update(ev.get("usage") or {})
206
- elif kind == "run_info":
288
+ return final_text, stats_usage
289
+
290
+ if kind == "run_info":
207
291
  self._handle_run_info_event(ev, meta, renderer)
292
+ return final_text, stats_usage
208
293
 
209
- return final_text, stats_usage
294
+ return None
210
295
 
211
296
  def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
212
297
  content = ev.get("content", "")
@@ -227,6 +312,14 @@ class AgentRunRenderingManager:
227
312
  meta["run_id"] = ev["run_id"]
228
313
  renderer.on_start(meta)
229
314
 
315
+ def _ensure_renderer_final_content(self, renderer: RichStreamRenderer, text: str) -> None:
316
+ """Populate renderer state with final output when the stream omits it."""
317
+ if not text:
318
+ return
319
+
320
+ text_value = _coerce_to_string(text)
321
+ _update_renderer_transcript(renderer, text_value)
322
+
230
323
  # --------------------------------------------------------------------- #
231
324
  # Finalisation helpers
232
325
  # --------------------------------------------------------------------- #
@@ -250,7 +343,7 @@ class AgentRunRenderingManager:
250
343
  if hasattr(renderer, "state") and hasattr(renderer.state, "buffer"):
251
344
  buffer_values = renderer.state.buffer
252
345
  elif hasattr(renderer, "buffer"):
253
- buffer_values = getattr(renderer, "buffer")
346
+ buffer_values = renderer.buffer
254
347
 
255
348
  if buffer_values is not None:
256
349
  try:
@@ -258,6 +351,10 @@ class AgentRunRenderingManager:
258
351
  except TypeError:
259
352
  rendered_text = ""
260
353
 
354
+ fallback_text = final_text or rendered_text
355
+ if fallback_text:
356
+ self._ensure_renderer_final_content(renderer, fallback_text)
357
+
261
358
  renderer.on_complete(st)
262
359
  return final_text or rendered_text or "No response content received."
263
360
 
glaip_sdk/client/tools.py CHANGED
@@ -96,9 +96,7 @@ class ToolClient(BaseClient):
96
96
  """
97
97
  return os.path.splitext(os.path.basename(file_path))[0]
98
98
 
99
- def _prepare_upload_data(
100
- self, name: str, framework: str, description: str | None = None, **kwargs
101
- ) -> dict:
99
+ def _prepare_upload_data(self, name: str, framework: str, description: str | None = None, **kwargs) -> dict:
102
100
  """Prepare upload data dictionary.
103
101
 
104
102
  Args:
@@ -217,29 +215,21 @@ class ToolClient(BaseClient):
217
215
  elif hasattr(current_tool, "description") and current_tool.description:
218
216
  update_data["description"] = current_tool.description
219
217
 
220
- def _handle_tags_update(
221
- self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool
222
- ) -> None:
218
+ def _handle_tags_update(self, update_data: dict[str, Any], kwargs: dict[str, Any], current_tool: Tool) -> None:
223
219
  """Handle tags field in update payload."""
224
220
  if kwargs.get("tags"):
225
221
  if isinstance(kwargs["tags"], list):
226
- update_data["tags"] = ",".join(
227
- str(tag).strip() for tag in kwargs["tags"]
228
- )
222
+ update_data["tags"] = ",".join(str(tag).strip() for tag in kwargs["tags"])
229
223
  else:
230
224
  update_data["tags"] = str(kwargs["tags"])
231
225
  elif hasattr(current_tool, "tags") and current_tool.tags:
232
226
  # Preserve existing tags if present
233
227
  if isinstance(current_tool.tags, list):
234
- update_data["tags"] = ",".join(
235
- str(tag).strip() for tag in current_tool.tags
236
- )
228
+ update_data["tags"] = ",".join(str(tag).strip() for tag in current_tool.tags)
237
229
  else:
238
230
  update_data["tags"] = str(current_tool.tags)
239
231
 
240
- def _handle_additional_kwargs(
241
- self, update_data: dict[str, Any], kwargs: dict[str, Any]
242
- ) -> None:
232
+ def _handle_additional_kwargs(self, update_data: dict[str, Any], kwargs: dict[str, Any]) -> None:
243
233
  """Handle additional kwargs in update payload."""
244
234
  excluded_keys = {
245
235
  "tags",
@@ -290,12 +280,8 @@ class ToolClient(BaseClient):
290
280
  update_data = {
291
281
  "name": name if name is not None else current_tool.name,
292
282
  "type": current_type,
293
- "framework": kwargs.get(
294
- "framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)
295
- ),
296
- "version": kwargs.get(
297
- "version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)
298
- ),
283
+ "framework": kwargs.get("framework", getattr(current_tool, "framework", DEFAULT_TOOL_FRAMEWORK)),
284
+ "version": kwargs.get("version", getattr(current_tool, "version", DEFAULT_TOOL_VERSION)),
299
285
  }
300
286
 
301
287
  # Handle description update
@@ -355,9 +341,7 @@ class ToolClient(BaseClient):
355
341
 
356
342
  try:
357
343
  # Prepare upload data
358
- upload_data = self._prepare_upload_data(
359
- name=name, framework=framework, description=description, **kwargs
360
- )
344
+ upload_data = self._prepare_upload_data(name=name, framework=framework, description=description, **kwargs)
361
345
 
362
346
  # Upload file
363
347
  return self._upload_tool_file(temp_file_path, upload_data)