glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.7__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 (116) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +156 -32
  3. glaip_sdk/cli/auth.py +14 -8
  4. glaip_sdk/cli/commands/accounts.py +1 -1
  5. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  6. glaip_sdk/cli/commands/agents/_common.py +561 -0
  7. glaip_sdk/cli/commands/agents/create.py +151 -0
  8. glaip_sdk/cli/commands/agents/delete.py +64 -0
  9. glaip_sdk/cli/commands/agents/get.py +89 -0
  10. glaip_sdk/cli/commands/agents/list.py +129 -0
  11. glaip_sdk/cli/commands/agents/run.py +264 -0
  12. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  13. glaip_sdk/cli/commands/agents/update.py +112 -0
  14. glaip_sdk/cli/commands/common_config.py +15 -12
  15. glaip_sdk/cli/commands/configure.py +2 -3
  16. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  17. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  18. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  19. glaip_sdk/cli/commands/mcps/create.py +152 -0
  20. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  21. glaip_sdk/cli/commands/mcps/get.py +212 -0
  22. glaip_sdk/cli/commands/mcps/list.py +69 -0
  23. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  24. glaip_sdk/cli/commands/mcps/update.py +190 -0
  25. glaip_sdk/cli/commands/models.py +2 -4
  26. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  27. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  28. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  29. glaip_sdk/cli/commands/tools/_common.py +80 -0
  30. glaip_sdk/cli/commands/tools/create.py +228 -0
  31. glaip_sdk/cli/commands/tools/delete.py +61 -0
  32. glaip_sdk/cli/commands/tools/get.py +103 -0
  33. glaip_sdk/cli/commands/tools/list.py +69 -0
  34. glaip_sdk/cli/commands/tools/script.py +49 -0
  35. glaip_sdk/cli/commands/tools/update.py +102 -0
  36. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  37. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  38. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  39. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  40. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  41. glaip_sdk/cli/commands/update.py +163 -17
  42. glaip_sdk/cli/core/output.py +12 -7
  43. glaip_sdk/cli/entrypoint.py +20 -0
  44. glaip_sdk/cli/main.py +127 -39
  45. glaip_sdk/cli/pager.py +3 -3
  46. glaip_sdk/cli/resolution.py +2 -1
  47. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  48. glaip_sdk/cli/slash/agent_session.py +5 -2
  49. glaip_sdk/cli/slash/prompt.py +11 -0
  50. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  51. glaip_sdk/cli/slash/session.py +58 -13
  52. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  53. glaip_sdk/cli/slash/tui/accounts.tcss +7 -5
  54. glaip_sdk/cli/slash/tui/accounts_app.py +70 -9
  55. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  56. glaip_sdk/cli/slash/tui/context.py +59 -0
  57. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  58. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  59. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  60. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  61. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  62. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  63. glaip_sdk/cli/slash/tui/toast.py +123 -0
  64. glaip_sdk/cli/transcript/history.py +1 -1
  65. glaip_sdk/cli/transcript/viewer.py +5 -3
  66. glaip_sdk/cli/update_notifier.py +215 -7
  67. glaip_sdk/cli/validators.py +1 -1
  68. glaip_sdk/client/__init__.py +2 -1
  69. glaip_sdk/client/_schedule_payloads.py +89 -0
  70. glaip_sdk/client/agents.py +50 -8
  71. glaip_sdk/client/hitl.py +136 -0
  72. glaip_sdk/client/main.py +7 -1
  73. glaip_sdk/client/mcps.py +44 -13
  74. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  75. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  76. glaip_sdk/client/payloads/agent/responses.py +43 -0
  77. glaip_sdk/client/run_rendering.py +367 -3
  78. glaip_sdk/client/schedules.py +439 -0
  79. glaip_sdk/client/tools.py +57 -26
  80. glaip_sdk/hitl/__init__.py +48 -0
  81. glaip_sdk/hitl/base.py +64 -0
  82. glaip_sdk/hitl/callback.py +43 -0
  83. glaip_sdk/hitl/local.py +121 -0
  84. glaip_sdk/hitl/remote.py +523 -0
  85. glaip_sdk/models/__init__.py +17 -0
  86. glaip_sdk/models/agent_runs.py +2 -1
  87. glaip_sdk/models/schedule.py +224 -0
  88. glaip_sdk/registry/tool.py +273 -59
  89. glaip_sdk/runner/__init__.py +20 -3
  90. glaip_sdk/runner/deps.py +5 -8
  91. glaip_sdk/runner/langgraph.py +317 -42
  92. glaip_sdk/runner/logging_config.py +77 -0
  93. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  94. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  95. glaip_sdk/schedules/__init__.py +22 -0
  96. glaip_sdk/schedules/base.py +291 -0
  97. glaip_sdk/tools/base.py +44 -11
  98. glaip_sdk/utils/__init__.py +1 -0
  99. glaip_sdk/utils/bundler.py +138 -2
  100. glaip_sdk/utils/import_resolver.py +43 -11
  101. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  102. glaip_sdk/utils/runtime_config.py +15 -12
  103. glaip_sdk/utils/sync.py +31 -11
  104. glaip_sdk/utils/tool_detection.py +274 -6
  105. glaip_sdk/utils/tool_storage_provider.py +140 -0
  106. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/METADATA +47 -37
  107. glaip_sdk-0.7.7.dist-info/RECORD +213 -0
  108. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.7.dist-info}/WHEEL +2 -1
  109. glaip_sdk-0.7.7.dist-info/entry_points.txt +2 -0
  110. glaip_sdk-0.7.7.dist-info/top_level.txt +1 -0
  111. glaip_sdk/cli/commands/agents.py +0 -1509
  112. glaip_sdk/cli/commands/mcps.py +0 -1356
  113. glaip_sdk/cli/commands/tools.py +0 -576
  114. glaip_sdk/cli/utils.py +0 -263
  115. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  116. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
@@ -9,9 +9,12 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import logging
12
- from collections.abc import Callable
12
+ from collections.abc import AsyncIterable, Callable
13
13
  from time import monotonic
14
- from typing import Any
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ if TYPE_CHECKING:
17
+ from glaip_sdk.hitl.remote import RemoteHITLHandler
15
18
 
16
19
  import httpx
17
20
  from rich.console import Console as _Console
@@ -30,6 +33,7 @@ from glaip_sdk.utils.rendering.renderer import (
30
33
  from glaip_sdk.utils.rendering.state import TranscriptBuffer
31
34
 
32
35
  NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
36
+ _FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
33
37
 
34
38
 
35
39
  def _coerce_to_string(value: Any) -> str:
@@ -129,6 +133,7 @@ class AgentRunRenderingManager:
129
133
  timeout_seconds: float,
130
134
  agent_name: str | None,
131
135
  meta: dict[str, Any],
136
+ hitl_handler: RemoteHITLHandler | None = None,
132
137
  ) -> tuple[str, dict[str, Any], float | None, float | None]:
133
138
  """Process streaming events and accumulate response."""
134
139
  final_text = ""
@@ -152,10 +157,14 @@ class AgentRunRenderingManager:
152
157
  final_text,
153
158
  stats_usage,
154
159
  meta,
160
+ hitl_handler=hitl_handler,
155
161
  )
156
162
 
157
163
  if controller and getattr(controller, "enabled", False):
158
164
  controller.poll(renderer)
165
+ parsed_event = self._parse_event(event)
166
+ if parsed_event and self._is_final_event(parsed_event):
167
+ break
159
168
  finally:
160
169
  if controller and getattr(controller, "enabled", False):
161
170
  controller.on_stream_complete()
@@ -163,6 +172,300 @@ class AgentRunRenderingManager:
163
172
  finished_monotonic = monotonic()
164
173
  return final_text, stats_usage, started_monotonic, finished_monotonic
165
174
 
175
+ async def async_process_stream_events(
176
+ self,
177
+ event_stream: AsyncIterable[dict[str, Any]],
178
+ renderer: RichStreamRenderer,
179
+ meta: dict[str, Any],
180
+ *,
181
+ skip_final_render: bool = True,
182
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
183
+ """Process streaming events from an async event source.
184
+
185
+ This method provides unified stream processing for both remote (HTTP)
186
+ and local (LangGraph) agent execution, ensuring consistent behavior.
187
+
188
+ Args:
189
+ event_stream: Async iterable yielding SSE-like event dicts.
190
+ Each event should have a "data" key with JSON string, or be
191
+ a pre-parsed dict with "content", "metadata", etc.
192
+ renderer: Renderer to use for displaying events.
193
+ meta: Metadata dictionary for renderer context.
194
+ skip_final_render: If True, skip rendering final_response events
195
+ (they are rendered separately via finalize_renderer).
196
+
197
+ Returns:
198
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
199
+ """
200
+ final_text = ""
201
+ stats_usage: dict[str, Any] = {}
202
+ started_monotonic: float | None = None
203
+ last_rendered_content: str | None = None
204
+
205
+ controller = getattr(renderer, "transcript_controller", None)
206
+ if controller and getattr(controller, "enabled", False):
207
+ controller.on_stream_start(renderer)
208
+
209
+ try:
210
+ async for event in event_stream:
211
+ if started_monotonic is None:
212
+ started_monotonic = monotonic()
213
+
214
+ # Parse event if needed (handles both raw SSE and pre-parsed dicts)
215
+ parsed_event = self._parse_event(event)
216
+ if parsed_event is None:
217
+ continue
218
+
219
+ # Process the event and update accumulators
220
+ final_text, stats_usage = self._handle_parsed_event(
221
+ parsed_event,
222
+ renderer,
223
+ final_text,
224
+ stats_usage,
225
+ meta,
226
+ skip_final_render=skip_final_render,
227
+ last_rendered_content=last_rendered_content,
228
+ )
229
+
230
+ # Track last rendered content to avoid duplicates
231
+ content_str = self._extract_content_string(parsed_event)
232
+ if content_str:
233
+ last_rendered_content = content_str
234
+
235
+ if controller and getattr(controller, "enabled", False):
236
+ controller.poll(renderer)
237
+ if parsed_event and self._is_final_event(parsed_event):
238
+ break
239
+ finally:
240
+ if controller and getattr(controller, "enabled", False):
241
+ controller.on_stream_complete()
242
+
243
+ finished_monotonic = monotonic()
244
+ return final_text, stats_usage, started_monotonic, finished_monotonic
245
+
246
+ def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
247
+ """Parse an SSE event dict into a usable format.
248
+
249
+ Args:
250
+ event: Raw event dict, either with "data" key (SSE format) or
251
+ pre-parsed with "content", "metadata", etc.
252
+
253
+ Returns:
254
+ Parsed event dict, or None if parsing fails.
255
+ """
256
+ if "data" in event:
257
+ try:
258
+ return json.loads(event["data"])
259
+ except json.JSONDecodeError:
260
+ self._logger.debug("Non-JSON SSE fragment skipped")
261
+ return None
262
+ # Already parsed (e.g., from local runner)
263
+ return event if event else None
264
+
265
+ def _handle_parsed_event(
266
+ self,
267
+ ev: dict[str, Any],
268
+ renderer: RichStreamRenderer,
269
+ final_text: str,
270
+ stats_usage: dict[str, Any],
271
+ meta: dict[str, Any],
272
+ *,
273
+ skip_final_render: bool = True,
274
+ last_rendered_content: str | None = None,
275
+ ) -> tuple[str, dict[str, Any]]:
276
+ """Handle a parsed event and update accumulators.
277
+
278
+ Args:
279
+ ev: Parsed event dictionary.
280
+ renderer: Renderer instance.
281
+ final_text: Current accumulated final text.
282
+ stats_usage: Usage statistics dictionary.
283
+ meta: Metadata dictionary.
284
+ skip_final_render: If True, skip rendering final_response events.
285
+ last_rendered_content: Last rendered content to avoid duplicates.
286
+
287
+ Returns:
288
+ Tuple of (updated_final_text, updated_stats_usage).
289
+ """
290
+ kind = self._get_event_kind(ev)
291
+
292
+ # Dispatch to specialized handlers based on event kind
293
+ handler = self._get_event_handler(kind, ev)
294
+ if handler:
295
+ return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
296
+
297
+ # Default: handle content events
298
+ return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
299
+
300
+ def _get_event_handler(
301
+ self,
302
+ kind: str | None,
303
+ ev: dict[str, Any],
304
+ ) -> Callable[..., tuple[str, dict[str, Any]]] | None:
305
+ """Get the appropriate handler for an event kind.
306
+
307
+ Args:
308
+ kind: Event kind string.
309
+ ev: Event dictionary (for checking is_final flag).
310
+
311
+ Returns:
312
+ Handler function or None for default content handling.
313
+ """
314
+ if kind == "usage":
315
+ return self._handle_usage_event
316
+ if kind == "final_response" or ev.get("is_final"):
317
+ return self._handle_final_response_event
318
+ if kind == "run_info":
319
+ return self._handle_run_info_event_wrapper
320
+ if kind in ("artifact", "status_update"):
321
+ return self._handle_render_only_event
322
+ return None
323
+
324
+ def _handle_usage_event(
325
+ self,
326
+ ev: dict[str, Any],
327
+ _renderer: RichStreamRenderer,
328
+ final_text: str,
329
+ stats_usage: dict[str, Any],
330
+ _meta: dict[str, Any],
331
+ _skip_final_render: bool,
332
+ ) -> tuple[str, dict[str, Any]]:
333
+ """Handle usage events."""
334
+ stats_usage.update(ev.get("usage") or {})
335
+ return final_text, stats_usage
336
+
337
+ def _handle_final_response_event(
338
+ self,
339
+ ev: dict[str, Any],
340
+ renderer: RichStreamRenderer,
341
+ final_text: str,
342
+ stats_usage: dict[str, Any],
343
+ _meta: dict[str, Any],
344
+ skip_final_render: bool,
345
+ ) -> tuple[str, dict[str, Any]]:
346
+ """Handle final_response events."""
347
+ content = ev.get("content")
348
+ if content:
349
+ final_text = str(content)
350
+ if not skip_final_render:
351
+ renderer.on_event(ev)
352
+ return final_text, stats_usage
353
+
354
+ def _handle_run_info_event_wrapper(
355
+ self,
356
+ ev: dict[str, Any],
357
+ renderer: RichStreamRenderer,
358
+ final_text: str,
359
+ stats_usage: dict[str, Any],
360
+ meta: dict[str, Any],
361
+ _skip_final_render: bool,
362
+ ) -> tuple[str, dict[str, Any]]:
363
+ """Handle run_info events."""
364
+ self._handle_run_info_event(ev, meta, renderer)
365
+ return final_text, stats_usage
366
+
367
+ def _handle_render_only_event(
368
+ self,
369
+ ev: dict[str, Any],
370
+ renderer: RichStreamRenderer,
371
+ final_text: str,
372
+ stats_usage: dict[str, Any],
373
+ _meta: dict[str, Any],
374
+ _skip_final_render: bool,
375
+ ) -> tuple[str, dict[str, Any]]:
376
+ """Handle events that only need rendering (artifact, status_update)."""
377
+ renderer.on_event(ev)
378
+ return final_text, stats_usage
379
+
380
+ def _handle_content_event_async(
381
+ self,
382
+ ev: dict[str, Any],
383
+ renderer: RichStreamRenderer,
384
+ final_text: str,
385
+ stats_usage: dict[str, Any],
386
+ last_rendered_content: str | None,
387
+ ) -> tuple[str, dict[str, Any]]:
388
+ """Handle content events with deduplication."""
389
+ content = ev.get("content")
390
+ if content:
391
+ content_str = str(content)
392
+ if not content_str.startswith("Artifact received:"):
393
+ kind = self._get_event_kind(ev)
394
+ # Skip accumulating content for status updates and agent steps
395
+ if kind in ("agent_step", "status_update"):
396
+ renderer.on_event(ev)
397
+ return final_text, stats_usage
398
+
399
+ if self._is_token_event(ev):
400
+ renderer.on_event(ev)
401
+ final_text = f"{final_text}{content_str}"
402
+ else:
403
+ if content_str != last_rendered_content:
404
+ renderer.on_event(ev)
405
+ final_text = content_str
406
+ else:
407
+ renderer.on_event(ev)
408
+ return final_text, stats_usage
409
+
410
+ def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
411
+ """Extract normalized event kind from parsed event.
412
+
413
+ Args:
414
+ ev: Parsed event dictionary.
415
+
416
+ Returns:
417
+ Event kind string or None.
418
+ """
419
+ metadata = ev.get("metadata") or {}
420
+ kind = metadata.get("kind")
421
+ if kind:
422
+ return str(kind)
423
+ event_type = ev.get("event_type")
424
+ return str(event_type) if event_type else None
425
+
426
+ def _is_token_event(self, ev: dict[str, Any]) -> bool:
427
+ """Return True when the event represents token streaming output.
428
+
429
+ Args:
430
+ ev: Parsed event dictionary.
431
+
432
+ Returns:
433
+ True when the event is a token chunk, otherwise False.
434
+ """
435
+ metadata = ev.get("metadata") or {}
436
+ kind = metadata.get("kind")
437
+ return str(kind).lower() == "token"
438
+
439
+ def _is_final_event(self, ev: dict[str, Any]) -> bool:
440
+ """Return True when the event marks stream termination.
441
+
442
+ Args:
443
+ ev: Parsed event dictionary.
444
+
445
+ Returns:
446
+ True when the event is terminal, otherwise False.
447
+ """
448
+ if ev.get("is_final") is True or ev.get("final") is True:
449
+ return True
450
+ kind = self._get_event_kind(ev)
451
+ return kind in _FINAL_EVENT_TYPES
452
+
453
+ def _extract_content_string(self, event: dict[str, Any]) -> str | None:
454
+ """Extract textual content from a parsed event.
455
+
456
+ Args:
457
+ event: Parsed event dictionary.
458
+
459
+ Returns:
460
+ Content string or None.
461
+ """
462
+ if not event:
463
+ return None
464
+ content = event.get("content")
465
+ if content:
466
+ return str(content)
467
+ return None
468
+
166
469
  def _capture_request_id(
167
470
  self,
168
471
  stream_response: httpx.Response,
@@ -206,6 +509,7 @@ class AgentRunRenderingManager:
206
509
  final_text: str,
207
510
  stats_usage: dict[str, Any],
208
511
  meta: dict[str, Any],
512
+ hitl_handler: RemoteHITLHandler | None = None,
209
513
  ) -> tuple[str, dict[str, Any]]:
210
514
  """Process a single streaming event.
211
515
 
@@ -215,6 +519,7 @@ class AgentRunRenderingManager:
215
519
  final_text: Accumulated text so far.
216
520
  stats_usage: Usage statistics dictionary.
217
521
  meta: Metadata dictionary.
522
+ hitl_handler: Optional HITL handler for approval callbacks.
218
523
 
219
524
  Returns:
220
525
  Tuple of (updated_final_text, updated_stats_usage).
@@ -225,6 +530,17 @@ class AgentRunRenderingManager:
225
530
  self._logger.debug("Non-JSON SSE fragment skipped")
226
531
  return final_text, stats_usage
227
532
 
533
+ # Handle HITL event (non-blocking via thread)
534
+ if hitl_handler and self._is_hitl_pending_event(ev):
535
+ try:
536
+ hitl_handler.handle_hitl_event(ev)
537
+ except Exception as e:
538
+ # Log but don't crash stream
539
+ self._logger.error(
540
+ f"HITL handler error: {e}",
541
+ exc_info=True,
542
+ )
543
+
228
544
  kind = (ev.get("metadata") or {}).get("kind")
229
545
  renderer.on_event(ev)
230
546
 
@@ -239,7 +555,9 @@ class AgentRunRenderingManager:
239
555
  if handled is not None:
240
556
  return handled
241
557
 
242
- if ev.get("content"):
558
+ # Only accumulate content for actual content events, not status updates or agent steps
559
+ # Status updates (agent_step) should be rendered but not accumulated in final_text
560
+ if ev.get("content") and kind not in ("agent_step", "status_update"):
243
561
  final_text = self._handle_content_event(ev, final_text)
244
562
 
245
563
  return final_text, stats_usage
@@ -285,9 +603,25 @@ class AgentRunRenderingManager:
285
603
  """
286
604
  content = ev.get("content", "")
287
605
  if not content.startswith("Artifact received:"):
606
+ if self._is_token_event(ev):
607
+ return f"{final_text}{content}"
288
608
  return content
289
609
  return final_text
290
610
 
611
+ @staticmethod
612
+ def _is_hitl_pending_event(event: dict[str, Any]) -> bool:
613
+ """Check if event is a pending HITL approval request.
614
+
615
+ Args:
616
+ event: Parsed event dictionary.
617
+
618
+ Returns:
619
+ True if event is a pending HITL request.
620
+ """
621
+ metadata = event.get("metadata", {})
622
+ hitl_meta = metadata.get("hitl", {})
623
+ return hitl_meta.get("required") is True and hitl_meta.get("decision") == "pending"
624
+
291
625
  def _handle_run_info_event(
292
626
  self,
293
627
  ev: dict[str, Any],
@@ -413,3 +747,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
413
747
  if not specified in kwargs.
414
748
  """
415
749
  return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
750
+
751
+
752
+ def finalize_render_manager(
753
+ manager: AgentRunRenderingManager,
754
+ renderer: RichStreamRenderer,
755
+ final_text: str,
756
+ stats_usage: dict[str, Any],
757
+ started_monotonic: float | None,
758
+ finished_monotonic: float | None,
759
+ ) -> str:
760
+ """Helper to finalize renderer via manager and return final text.
761
+
762
+ Args:
763
+ manager: The rendering manager instance.
764
+ renderer: Renderer to finalize.
765
+ final_text: Final text content.
766
+ stats_usage: Usage statistics dictionary.
767
+ started_monotonic: Start time (monotonic).
768
+ finished_monotonic: Finish time (monotonic).
769
+
770
+ Returns:
771
+ Final text string.
772
+ """
773
+ return manager.finalize_renderer(
774
+ renderer,
775
+ final_text,
776
+ stats_usage,
777
+ started_monotonic,
778
+ finished_monotonic,
779
+ )