glaip-sdk 0.6.10__py3-none-any.whl → 0.6.19__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.
glaip_sdk/__init__.py CHANGED
@@ -4,12 +4,49 @@ Authors:
4
4
  Raymond Christopher (raymond.christopher@gdplabs.id)
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from typing import TYPE_CHECKING, Any
11
+
7
12
  from glaip_sdk._version import __version__
8
- from glaip_sdk.client import Client
9
- from glaip_sdk.exceptions import AIPError
10
- from glaip_sdk.agents import Agent
11
- from glaip_sdk.tools import Tool
12
- from glaip_sdk.mcps import MCP
13
13
 
14
+ if TYPE_CHECKING: # pragma: no cover - import only for type checking
15
+ from glaip_sdk.agents import Agent
16
+ from glaip_sdk.client import Client
17
+ from glaip_sdk.exceptions import AIPError
18
+ from glaip_sdk.mcps import MCP
19
+ from glaip_sdk.tools import Tool
14
20
 
15
21
  __all__ = ["Client", "Agent", "Tool", "MCP", "AIPError", "__version__"]
22
+
23
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
24
+ "Client": ("glaip_sdk.client", "Client"),
25
+ "Agent": ("glaip_sdk.agents", "Agent"),
26
+ "Tool": ("glaip_sdk.tools", "Tool"),
27
+ "MCP": ("glaip_sdk.mcps", "MCP"),
28
+ "AIPError": ("glaip_sdk.exceptions", "AIPError"),
29
+ }
30
+
31
+
32
+ def __getattr__(name: str) -> Any:
33
+ """Lazy attribute access for public SDK symbols to defer heavy imports."""
34
+ if name == "__version__":
35
+ # Import __version__ when accessed via __getattr__
36
+ # This ensures coverage even if __version__ was removed from __dict__ for testing
37
+ from glaip_sdk._version import __version__ as version # noqa: PLC0415
38
+
39
+ globals()["__version__"] = version
40
+ return version
41
+ if name in _LAZY_IMPORTS:
42
+ module_path, attr_name = _LAZY_IMPORTS[name]
43
+ module = importlib.import_module(module_path)
44
+ attr = getattr(module, attr_name)
45
+ globals()[name] = attr
46
+ return attr
47
+ raise AttributeError(f"module 'glaip_sdk' has no attribute {name!r}")
48
+
49
+
50
+ def __dir__() -> list[str]:
51
+ """Return module attributes for dir()."""
52
+ return sorted(__all__)
glaip_sdk/agents/base.py CHANGED
@@ -52,14 +52,7 @@ from pathlib import Path
52
52
  from typing import TYPE_CHECKING, Any
53
53
 
54
54
  from glaip_sdk.registry import get_agent_registry, get_mcp_registry, get_tool_registry
55
- from glaip_sdk.runner import get_default_runner
56
- from glaip_sdk.runner.deps import (
57
- check_local_runtime_available,
58
- get_local_runtime_missing_message,
59
- )
60
- from glaip_sdk.utils.discovery import find_agent
61
55
  from glaip_sdk.utils.resource_refs import is_uuid
62
- from glaip_sdk.utils.runtime_config import normalize_runtime_config_keys
63
56
 
64
57
  if TYPE_CHECKING:
65
58
  from glaip_sdk.models import AgentResponse
@@ -520,6 +513,8 @@ class Agent:
520
513
  from glaip_sdk.utils.client import get_client # noqa: PLC0415
521
514
 
522
515
  client = get_client()
516
+ from glaip_sdk.utils.discovery import find_agent # noqa: PLC0415
517
+
523
518
  response = self._create_or_update_agent(config, client, find_agent)
524
519
 
525
520
  # Update self with deployed info
@@ -885,6 +880,10 @@ class Agent:
885
880
  }
886
881
 
887
882
  if runtime_config is not None:
883
+ from glaip_sdk.utils.runtime_config import ( # noqa: PLC0415
884
+ normalize_runtime_config_keys,
885
+ )
886
+
888
887
  call_kwargs["runtime_config"] = normalize_runtime_config_keys(
889
888
  runtime_config,
890
889
  tool_registry=get_tool_registry(),
@@ -904,6 +903,12 @@ class Agent:
904
903
  Raises:
905
904
  ValueError: If local runtime is not available.
906
905
  """
906
+ from glaip_sdk.runner import get_default_runner # noqa: PLC0415
907
+ from glaip_sdk.runner.deps import ( # noqa: PLC0415
908
+ check_local_runtime_available,
909
+ get_local_runtime_missing_message,
910
+ )
911
+
907
912
  if check_local_runtime_available():
908
913
  return get_default_runner()
909
914
  raise ValueError(f"{_AGENT_NOT_DEPLOYED_MSG}\n\n{get_local_runtime_missing_message()}")
@@ -1,14 +1,19 @@
1
1
  """Shared helpers for configuration/account flows."""
2
2
 
3
- import click
3
+ from __future__ import annotations
4
+
4
5
  import logging
6
+ from typing import TYPE_CHECKING
7
+
8
+ import click
5
9
  from rich.console import Console
6
10
  from rich.text import Text
7
-
8
- from glaip_sdk import Client
9
11
  from glaip_sdk.branding import PRIMARY, SUCCESS_STYLE, WARNING_STYLE, AIPBranding
10
12
  from glaip_sdk.cli.utils import sdk_version
11
13
 
14
+ if TYPE_CHECKING: # pragma: no cover - type checking only
15
+ from glaip_sdk import Client
16
+
12
17
 
13
18
  def render_branding_header(console: Console, rule_text: str) -> None:
14
19
  """Render the standard CLI branding header with a custom rule text."""
@@ -36,11 +41,10 @@ def check_connection(
36
41
  console.print("\n🔌 Testing connection...")
37
42
  client: Client | None = None
38
43
  try:
39
- # Import lazily so test patches targeting glaip_sdk.Client are honored
40
- from importlib import import_module # noqa: PLC0415
44
+ # Import lazily to avoid pulling in SDK dependencies during CLI startup.
45
+ from glaip_sdk import Client # noqa: PLC0415
41
46
 
42
- client_module = import_module("glaip_sdk")
43
- client = client_module.Client(api_url=api_url, api_key=api_key)
47
+ client = Client(api_url=api_url, api_key=api_key)
44
48
  try:
45
49
  agents = client.list_agents()
46
50
  console.print(Text(f"✅ Connection successful! Found {len(agents)} agents", style=SUCCESS_STYLE))
@@ -75,11 +79,10 @@ def check_connection_with_reason(
75
79
  """Test connectivity and return structured reason."""
76
80
  client: Client | None = None
77
81
  try:
78
- # Import lazily so test patches targeting glaip_sdk.Client are honored
79
- from importlib import import_module # noqa: PLC0415
82
+ # Import lazily to avoid pulling in SDK dependencies during CLI startup.
83
+ from glaip_sdk import Client # noqa: PLC0415
80
84
 
81
- client_module = import_module("glaip_sdk")
82
- client = client_module.Client(api_url=api_url, api_key=api_key)
85
+ client = Client(api_url=api_url, api_key=api_key)
83
86
  try:
84
87
  client.list_agents()
85
88
  return True, ""
@@ -29,12 +29,6 @@ from glaip_sdk.cli.io import export_resource_to_file_with_validation
29
29
  from glaip_sdk.cli.rich_helpers import markup_text, print_markup
30
30
  from glaip_sdk.rich_components import AIPPanel, AIPTable
31
31
  from glaip_sdk.utils import format_datetime, is_uuid
32
-
33
- try:
34
- from glaip_sdk import _version as _version_module
35
- except ImportError: # pragma: no cover - defensive import
36
- _version_module = None
37
-
38
32
  from .prompting import (
39
33
  _fuzzy_pick,
40
34
  _fuzzy_pick_for_resources,
@@ -43,6 +37,9 @@ from .prompting import (
43
37
  )
44
38
  from .rendering import _spinner_stop, _spinner_update, spinner_context
45
39
 
40
+ _VERSION_MODULE_MISSING = object()
41
+ _version_module: Any | None = _VERSION_MODULE_MISSING
42
+
46
43
  console = Console()
47
44
  pager.console = console
48
45
  logger = logging.getLogger("glaip_sdk.cli.core.output")
@@ -236,7 +233,15 @@ def handle_resource_export(
236
233
 
237
234
  def sdk_version() -> str:
238
235
  """Return the current SDK version, warning if metadata is unavailable."""
239
- global _WARNED_SDK_VERSION_FALLBACK
236
+ global _WARNED_SDK_VERSION_FALLBACK, _version_module
237
+
238
+ if _version_module is _VERSION_MODULE_MISSING:
239
+ try:
240
+ from importlib import import_module # noqa: PLC0415
241
+
242
+ _version_module = import_module("glaip_sdk._version")
243
+ except Exception: # pragma: no cover - defensive fallback
244
+ _version_module = None
240
245
 
241
246
  if _version_module is None:
242
247
  if not _WARNED_SDK_VERSION_FALLBACK:
glaip_sdk/cli/main.py CHANGED
@@ -11,8 +11,6 @@ from typing import Any
11
11
 
12
12
  import click
13
13
  from rich.console import Console
14
-
15
- from glaip_sdk import Client
16
14
  from glaip_sdk.branding import (
17
15
  ERROR,
18
16
  ERROR_STYLE,
@@ -49,6 +47,18 @@ from glaip_sdk.config.constants import (
49
47
  from glaip_sdk.icons import ICON_AGENT
50
48
  from glaip_sdk.rich_components import AIPPanel, AIPTable
51
49
 
50
+ Client: type[Any] | None = None
51
+
52
+
53
+ def _resolve_client_class() -> type[Any]:
54
+ """Resolve the Client class lazily to avoid heavy imports at CLI startup."""
55
+ global Client
56
+ if Client is None:
57
+ from glaip_sdk import Client as ClientClass # noqa: PLC0415
58
+
59
+ Client = ClientClass
60
+ return Client
61
+
52
62
 
53
63
  def _suppress_chatty_loggers() -> None:
54
64
  """Silence noisy SDK/httpx logs for CLI output."""
@@ -339,14 +349,15 @@ def _safe_list_call(obj: Any, attr: str) -> list[Any]:
339
349
 
340
350
  def _get_client_from_config(config: dict) -> Any:
341
351
  """Return a Client instance built from config."""
342
- return Client(
352
+ client_class = _resolve_client_class()
353
+ return client_class(
343
354
  api_url=config["api_url"],
344
355
  api_key=config["api_key"],
345
356
  timeout=config.get("timeout", 30.0),
346
357
  )
347
358
 
348
359
 
349
- def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Client:
360
+ def _create_and_test_client(config: dict, console: Console, *, compact: bool = False) -> Any:
350
361
  """Create client and test connection by fetching resources."""
351
362
  client: Any = _get_client_from_config(config)
352
363
 
@@ -453,13 +453,11 @@ class AgentClient(BaseClient):
453
453
  Returns:
454
454
  Final text string.
455
455
  """
456
+ from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
457
+
456
458
  manager = self._get_renderer_manager()
457
- return manager.finalize_renderer(
458
- renderer,
459
- final_text,
460
- stats_usage,
461
- started_monotonic,
462
- finished_monotonic,
459
+ return finalize_render_manager(
460
+ manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
463
461
  )
464
462
 
465
463
  def _get_tool_client(self) -> ToolClient:
@@ -9,7 +9,7 @@ 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
14
  from typing import Any
15
15
 
@@ -30,6 +30,7 @@ from glaip_sdk.utils.rendering.renderer import (
30
30
  from glaip_sdk.utils.rendering.state import TranscriptBuffer
31
31
 
32
32
  NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
33
+ _FINAL_EVENT_TYPES = {"final_response", "error", "step_limit_exceeded"}
33
34
 
34
35
 
35
36
  def _coerce_to_string(value: Any) -> str:
@@ -156,6 +157,9 @@ class AgentRunRenderingManager:
156
157
 
157
158
  if controller and getattr(controller, "enabled", False):
158
159
  controller.poll(renderer)
160
+ parsed_event = self._parse_event(event)
161
+ if parsed_event and self._is_final_event(parsed_event):
162
+ break
159
163
  finally:
160
164
  if controller and getattr(controller, "enabled", False):
161
165
  controller.on_stream_complete()
@@ -163,6 +167,300 @@ class AgentRunRenderingManager:
163
167
  finished_monotonic = monotonic()
164
168
  return final_text, stats_usage, started_monotonic, finished_monotonic
165
169
 
170
+ async def async_process_stream_events(
171
+ self,
172
+ event_stream: AsyncIterable[dict[str, Any]],
173
+ renderer: RichStreamRenderer,
174
+ meta: dict[str, Any],
175
+ *,
176
+ skip_final_render: bool = True,
177
+ ) -> tuple[str, dict[str, Any], float | None, float | None]:
178
+ """Process streaming events from an async event source.
179
+
180
+ This method provides unified stream processing for both remote (HTTP)
181
+ and local (LangGraph) agent execution, ensuring consistent behavior.
182
+
183
+ Args:
184
+ event_stream: Async iterable yielding SSE-like event dicts.
185
+ Each event should have a "data" key with JSON string, or be
186
+ a pre-parsed dict with "content", "metadata", etc.
187
+ renderer: Renderer to use for displaying events.
188
+ meta: Metadata dictionary for renderer context.
189
+ skip_final_render: If True, skip rendering final_response events
190
+ (they are rendered separately via finalize_renderer).
191
+
192
+ Returns:
193
+ Tuple of (final_text, stats_usage, started_monotonic, finished_monotonic).
194
+ """
195
+ final_text = ""
196
+ stats_usage: dict[str, Any] = {}
197
+ started_monotonic: float | None = None
198
+ last_rendered_content: str | None = None
199
+
200
+ controller = getattr(renderer, "transcript_controller", None)
201
+ if controller and getattr(controller, "enabled", False):
202
+ controller.on_stream_start(renderer)
203
+
204
+ try:
205
+ async for event in event_stream:
206
+ if started_monotonic is None:
207
+ started_monotonic = monotonic()
208
+
209
+ # Parse event if needed (handles both raw SSE and pre-parsed dicts)
210
+ parsed_event = self._parse_event(event)
211
+ if parsed_event is None:
212
+ continue
213
+
214
+ # Process the event and update accumulators
215
+ final_text, stats_usage = self._handle_parsed_event(
216
+ parsed_event,
217
+ renderer,
218
+ final_text,
219
+ stats_usage,
220
+ meta,
221
+ skip_final_render=skip_final_render,
222
+ last_rendered_content=last_rendered_content,
223
+ )
224
+
225
+ # Track last rendered content to avoid duplicates
226
+ content_str = self._extract_content_string(parsed_event)
227
+ if content_str:
228
+ last_rendered_content = content_str
229
+
230
+ if controller and getattr(controller, "enabled", False):
231
+ controller.poll(renderer)
232
+ if parsed_event and self._is_final_event(parsed_event):
233
+ break
234
+ finally:
235
+ if controller and getattr(controller, "enabled", False):
236
+ controller.on_stream_complete()
237
+
238
+ finished_monotonic = monotonic()
239
+ return final_text, stats_usage, started_monotonic, finished_monotonic
240
+
241
+ def _parse_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
242
+ """Parse an SSE event dict into a usable format.
243
+
244
+ Args:
245
+ event: Raw event dict, either with "data" key (SSE format) or
246
+ pre-parsed with "content", "metadata", etc.
247
+
248
+ Returns:
249
+ Parsed event dict, or None if parsing fails.
250
+ """
251
+ if "data" in event:
252
+ try:
253
+ return json.loads(event["data"])
254
+ except json.JSONDecodeError:
255
+ self._logger.debug("Non-JSON SSE fragment skipped")
256
+ return None
257
+ # Already parsed (e.g., from local runner)
258
+ return event if event else None
259
+
260
+ def _handle_parsed_event(
261
+ self,
262
+ ev: dict[str, Any],
263
+ renderer: RichStreamRenderer,
264
+ final_text: str,
265
+ stats_usage: dict[str, Any],
266
+ meta: dict[str, Any],
267
+ *,
268
+ skip_final_render: bool = True,
269
+ last_rendered_content: str | None = None,
270
+ ) -> tuple[str, dict[str, Any]]:
271
+ """Handle a parsed event and update accumulators.
272
+
273
+ Args:
274
+ ev: Parsed event dictionary.
275
+ renderer: Renderer instance.
276
+ final_text: Current accumulated final text.
277
+ stats_usage: Usage statistics dictionary.
278
+ meta: Metadata dictionary.
279
+ skip_final_render: If True, skip rendering final_response events.
280
+ last_rendered_content: Last rendered content to avoid duplicates.
281
+
282
+ Returns:
283
+ Tuple of (updated_final_text, updated_stats_usage).
284
+ """
285
+ kind = self._get_event_kind(ev)
286
+
287
+ # Dispatch to specialized handlers based on event kind
288
+ handler = self._get_event_handler(kind, ev)
289
+ if handler:
290
+ return handler(ev, renderer, final_text, stats_usage, meta, skip_final_render)
291
+
292
+ # Default: handle content events
293
+ return self._handle_content_event_async(ev, renderer, final_text, stats_usage, last_rendered_content)
294
+
295
+ def _get_event_handler(
296
+ self,
297
+ kind: str | None,
298
+ ev: dict[str, Any],
299
+ ) -> Callable[..., tuple[str, dict[str, Any]]] | None:
300
+ """Get the appropriate handler for an event kind.
301
+
302
+ Args:
303
+ kind: Event kind string.
304
+ ev: Event dictionary (for checking is_final flag).
305
+
306
+ Returns:
307
+ Handler function or None for default content handling.
308
+ """
309
+ if kind == "usage":
310
+ return self._handle_usage_event
311
+ if kind == "final_response" or ev.get("is_final"):
312
+ return self._handle_final_response_event
313
+ if kind == "run_info":
314
+ return self._handle_run_info_event_wrapper
315
+ if kind in ("artifact", "status_update"):
316
+ return self._handle_render_only_event
317
+ return None
318
+
319
+ def _handle_usage_event(
320
+ self,
321
+ ev: dict[str, Any],
322
+ _renderer: RichStreamRenderer,
323
+ final_text: str,
324
+ stats_usage: dict[str, Any],
325
+ _meta: dict[str, Any],
326
+ _skip_final_render: bool,
327
+ ) -> tuple[str, dict[str, Any]]:
328
+ """Handle usage events."""
329
+ stats_usage.update(ev.get("usage") or {})
330
+ return final_text, stats_usage
331
+
332
+ def _handle_final_response_event(
333
+ self,
334
+ ev: dict[str, Any],
335
+ renderer: RichStreamRenderer,
336
+ final_text: str,
337
+ stats_usage: dict[str, Any],
338
+ _meta: dict[str, Any],
339
+ skip_final_render: bool,
340
+ ) -> tuple[str, dict[str, Any]]:
341
+ """Handle final_response events."""
342
+ content = ev.get("content")
343
+ if content:
344
+ final_text = str(content)
345
+ if not skip_final_render:
346
+ renderer.on_event(ev)
347
+ return final_text, stats_usage
348
+
349
+ def _handle_run_info_event_wrapper(
350
+ self,
351
+ ev: dict[str, Any],
352
+ renderer: RichStreamRenderer,
353
+ final_text: str,
354
+ stats_usage: dict[str, Any],
355
+ meta: dict[str, Any],
356
+ _skip_final_render: bool,
357
+ ) -> tuple[str, dict[str, Any]]:
358
+ """Handle run_info events."""
359
+ self._handle_run_info_event(ev, meta, renderer)
360
+ return final_text, stats_usage
361
+
362
+ def _handle_render_only_event(
363
+ self,
364
+ ev: dict[str, Any],
365
+ renderer: RichStreamRenderer,
366
+ final_text: str,
367
+ stats_usage: dict[str, Any],
368
+ _meta: dict[str, Any],
369
+ _skip_final_render: bool,
370
+ ) -> tuple[str, dict[str, Any]]:
371
+ """Handle events that only need rendering (artifact, status_update)."""
372
+ renderer.on_event(ev)
373
+ return final_text, stats_usage
374
+
375
+ def _handle_content_event_async(
376
+ self,
377
+ ev: dict[str, Any],
378
+ renderer: RichStreamRenderer,
379
+ final_text: str,
380
+ stats_usage: dict[str, Any],
381
+ last_rendered_content: str | None,
382
+ ) -> tuple[str, dict[str, Any]]:
383
+ """Handle content events with deduplication."""
384
+ content = ev.get("content")
385
+ if content:
386
+ content_str = str(content)
387
+ if not content_str.startswith("Artifact received:"):
388
+ kind = self._get_event_kind(ev)
389
+ # Skip accumulating content for status updates and agent steps
390
+ if kind in ("agent_step", "status_update"):
391
+ renderer.on_event(ev)
392
+ return final_text, stats_usage
393
+
394
+ if self._is_token_event(ev):
395
+ renderer.on_event(ev)
396
+ final_text = f"{final_text}{content_str}"
397
+ else:
398
+ if content_str != last_rendered_content:
399
+ renderer.on_event(ev)
400
+ final_text = content_str
401
+ else:
402
+ renderer.on_event(ev)
403
+ return final_text, stats_usage
404
+
405
+ def _get_event_kind(self, ev: dict[str, Any]) -> str | None:
406
+ """Extract normalized event kind from parsed event.
407
+
408
+ Args:
409
+ ev: Parsed event dictionary.
410
+
411
+ Returns:
412
+ Event kind string or None.
413
+ """
414
+ metadata = ev.get("metadata") or {}
415
+ kind = metadata.get("kind")
416
+ if kind:
417
+ return str(kind)
418
+ event_type = ev.get("event_type")
419
+ return str(event_type) if event_type else None
420
+
421
+ def _is_token_event(self, ev: dict[str, Any]) -> bool:
422
+ """Return True when the event represents token streaming output.
423
+
424
+ Args:
425
+ ev: Parsed event dictionary.
426
+
427
+ Returns:
428
+ True when the event is a token chunk, otherwise False.
429
+ """
430
+ metadata = ev.get("metadata") or {}
431
+ kind = metadata.get("kind")
432
+ return str(kind).lower() == "token"
433
+
434
+ def _is_final_event(self, ev: dict[str, Any]) -> bool:
435
+ """Return True when the event marks stream termination.
436
+
437
+ Args:
438
+ ev: Parsed event dictionary.
439
+
440
+ Returns:
441
+ True when the event is terminal, otherwise False.
442
+ """
443
+ if ev.get("is_final") is True or ev.get("final") is True:
444
+ return True
445
+ kind = self._get_event_kind(ev)
446
+ return kind in _FINAL_EVENT_TYPES
447
+
448
+ def _extract_content_string(self, event: dict[str, Any]) -> str | None:
449
+ """Extract textual content from a parsed event.
450
+
451
+ Args:
452
+ event: Parsed event dictionary.
453
+
454
+ Returns:
455
+ Content string or None.
456
+ """
457
+ if not event:
458
+ return None
459
+ content = event.get("content")
460
+ if content:
461
+ return str(content)
462
+ return None
463
+
166
464
  def _capture_request_id(
167
465
  self,
168
466
  stream_response: httpx.Response,
@@ -239,7 +537,9 @@ class AgentRunRenderingManager:
239
537
  if handled is not None:
240
538
  return handled
241
539
 
242
- if ev.get("content"):
540
+ # Only accumulate content for actual content events, not status updates or agent steps
541
+ # Status updates (agent_step) should be rendered but not accumulated in final_text
542
+ if ev.get("content") and kind not in ("agent_step", "status_update"):
243
543
  final_text = self._handle_content_event(ev, final_text)
244
544
 
245
545
  return final_text, stats_usage
@@ -285,6 +585,8 @@ class AgentRunRenderingManager:
285
585
  """
286
586
  content = ev.get("content", "")
287
587
  if not content.startswith("Artifact received:"):
588
+ if self._is_token_event(ev):
589
+ return f"{final_text}{content}"
288
590
  return content
289
591
  return final_text
290
592
 
@@ -413,3 +715,33 @@ def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
413
715
  if not specified in kwargs.
414
716
  """
415
717
  return kwargs.get("timeout", DEFAULT_AGENT_RUN_TIMEOUT)
718
+
719
+
720
+ def finalize_render_manager(
721
+ manager: AgentRunRenderingManager,
722
+ renderer: RichStreamRenderer,
723
+ final_text: str,
724
+ stats_usage: dict[str, Any],
725
+ started_monotonic: float | None,
726
+ finished_monotonic: float | None,
727
+ ) -> str:
728
+ """Helper to finalize renderer via manager and return final text.
729
+
730
+ Args:
731
+ manager: The rendering manager instance.
732
+ renderer: Renderer to finalize.
733
+ final_text: Final text content.
734
+ stats_usage: Usage statistics dictionary.
735
+ started_monotonic: Start time (monotonic).
736
+ finished_monotonic: Finish time (monotonic).
737
+
738
+ Returns:
739
+ Final text string.
740
+ """
741
+ return manager.finalize_renderer(
742
+ renderer,
743
+ final_text,
744
+ stats_usage,
745
+ started_monotonic,
746
+ finished_monotonic,
747
+ )
@@ -0,0 +1,15 @@
1
+ """Human-in-the-Loop (HITL) utilities for glaip-sdk.
2
+
3
+ This package provides utilities for HITL approval workflows in both local
4
+ and remote agent execution modes.
5
+
6
+ For local development, LocalPromptHandler is automatically injected when
7
+ agent_config.hitl_enabled is True. No manual setup required.
8
+
9
+ Authors:
10
+ Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
11
+ """
12
+
13
+ from glaip_sdk.hitl.local import LocalPromptHandler, PauseResumeCallback
14
+
15
+ __all__ = ["LocalPromptHandler", "PauseResumeCallback"]