glaip-sdk 0.1.2__py3-none-any.whl → 0.7.17__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 (217) hide show
  1. glaip_sdk/__init__.py +44 -4
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1413 -0
  5. glaip_sdk/branding.py +126 -2
  6. glaip_sdk/cli/account_store.py +555 -0
  7. glaip_sdk/cli/auth.py +260 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  11. glaip_sdk/cli/commands/agents/_common.py +562 -0
  12. glaip_sdk/cli/commands/agents/create.py +155 -0
  13. glaip_sdk/cli/commands/agents/delete.py +64 -0
  14. glaip_sdk/cli/commands/agents/get.py +89 -0
  15. glaip_sdk/cli/commands/agents/list.py +129 -0
  16. glaip_sdk/cli/commands/agents/run.py +264 -0
  17. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  18. glaip_sdk/cli/commands/agents/update.py +112 -0
  19. glaip_sdk/cli/commands/common_config.py +104 -0
  20. glaip_sdk/cli/commands/configure.py +728 -113
  21. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  22. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  23. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  24. glaip_sdk/cli/commands/mcps/create.py +152 -0
  25. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  26. glaip_sdk/cli/commands/mcps/get.py +212 -0
  27. glaip_sdk/cli/commands/mcps/list.py +69 -0
  28. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  29. glaip_sdk/cli/commands/mcps/update.py +190 -0
  30. glaip_sdk/cli/commands/models.py +12 -8
  31. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  32. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  33. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  34. glaip_sdk/cli/commands/tools/_common.py +80 -0
  35. glaip_sdk/cli/commands/tools/create.py +228 -0
  36. glaip_sdk/cli/commands/tools/delete.py +61 -0
  37. glaip_sdk/cli/commands/tools/get.py +103 -0
  38. glaip_sdk/cli/commands/tools/list.py +69 -0
  39. glaip_sdk/cli/commands/tools/script.py +49 -0
  40. glaip_sdk/cli/commands/tools/update.py +102 -0
  41. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  42. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  43. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  44. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  45. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  46. glaip_sdk/cli/commands/update.py +163 -17
  47. glaip_sdk/cli/config.py +49 -4
  48. glaip_sdk/cli/constants.py +38 -0
  49. glaip_sdk/cli/context.py +8 -0
  50. glaip_sdk/cli/core/__init__.py +79 -0
  51. glaip_sdk/cli/core/context.py +124 -0
  52. glaip_sdk/cli/core/output.py +851 -0
  53. glaip_sdk/cli/core/prompting.py +649 -0
  54. glaip_sdk/cli/core/rendering.py +187 -0
  55. glaip_sdk/cli/display.py +41 -20
  56. glaip_sdk/cli/entrypoint.py +20 -0
  57. glaip_sdk/cli/hints.py +57 -0
  58. glaip_sdk/cli/io.py +6 -3
  59. glaip_sdk/cli/main.py +340 -143
  60. glaip_sdk/cli/masking.py +21 -33
  61. glaip_sdk/cli/pager.py +12 -13
  62. glaip_sdk/cli/parsers/__init__.py +1 -3
  63. glaip_sdk/cli/resolution.py +2 -1
  64. glaip_sdk/cli/slash/__init__.py +0 -9
  65. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  66. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  67. glaip_sdk/cli/slash/agent_session.py +62 -21
  68. glaip_sdk/cli/slash/prompt.py +21 -0
  69. glaip_sdk/cli/slash/remote_runs_controller.py +568 -0
  70. glaip_sdk/cli/slash/session.py +1105 -153
  71. glaip_sdk/cli/slash/tui/__init__.py +36 -0
  72. glaip_sdk/cli/slash/tui/accounts.tcss +177 -0
  73. glaip_sdk/cli/slash/tui/accounts_app.py +1853 -0
  74. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  75. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  76. glaip_sdk/cli/slash/tui/context.py +92 -0
  77. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  78. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  79. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  80. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  81. glaip_sdk/cli/slash/tui/loading.py +80 -0
  82. glaip_sdk/cli/slash/tui/remote_runs_app.py +760 -0
  83. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  84. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  85. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  86. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  87. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  88. glaip_sdk/cli/slash/tui/toast.py +388 -0
  89. glaip_sdk/cli/transcript/__init__.py +12 -52
  90. glaip_sdk/cli/transcript/cache.py +255 -44
  91. glaip_sdk/cli/transcript/capture.py +66 -1
  92. glaip_sdk/cli/transcript/history.py +815 -0
  93. glaip_sdk/cli/transcript/viewer.py +72 -463
  94. glaip_sdk/cli/tui_settings.py +125 -0
  95. glaip_sdk/cli/update_notifier.py +227 -10
  96. glaip_sdk/cli/validators.py +5 -6
  97. glaip_sdk/client/__init__.py +3 -1
  98. glaip_sdk/client/_schedule_payloads.py +89 -0
  99. glaip_sdk/client/agent_runs.py +147 -0
  100. glaip_sdk/client/agents.py +576 -44
  101. glaip_sdk/client/base.py +26 -0
  102. glaip_sdk/client/hitl.py +136 -0
  103. glaip_sdk/client/main.py +25 -14
  104. glaip_sdk/client/mcps.py +165 -24
  105. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  106. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +63 -47
  107. glaip_sdk/client/payloads/agent/responses.py +43 -0
  108. glaip_sdk/client/run_rendering.py +546 -92
  109. glaip_sdk/client/schedules.py +439 -0
  110. glaip_sdk/client/shared.py +21 -0
  111. glaip_sdk/client/tools.py +206 -32
  112. glaip_sdk/config/constants.py +33 -2
  113. glaip_sdk/guardrails/__init__.py +80 -0
  114. glaip_sdk/guardrails/serializer.py +89 -0
  115. glaip_sdk/hitl/__init__.py +48 -0
  116. glaip_sdk/hitl/base.py +64 -0
  117. glaip_sdk/hitl/callback.py +43 -0
  118. glaip_sdk/hitl/local.py +121 -0
  119. glaip_sdk/hitl/remote.py +523 -0
  120. glaip_sdk/mcps/__init__.py +21 -0
  121. glaip_sdk/mcps/base.py +345 -0
  122. glaip_sdk/models/__init__.py +136 -0
  123. glaip_sdk/models/_provider_mappings.py +101 -0
  124. glaip_sdk/models/_validation.py +97 -0
  125. glaip_sdk/models/agent.py +48 -0
  126. glaip_sdk/models/agent_runs.py +117 -0
  127. glaip_sdk/models/common.py +42 -0
  128. glaip_sdk/models/constants.py +141 -0
  129. glaip_sdk/models/mcp.py +33 -0
  130. glaip_sdk/models/model.py +170 -0
  131. glaip_sdk/models/schedule.py +224 -0
  132. glaip_sdk/models/tool.py +33 -0
  133. glaip_sdk/payload_schemas/__init__.py +1 -13
  134. glaip_sdk/payload_schemas/agent.py +1 -0
  135. glaip_sdk/payload_schemas/guardrails.py +34 -0
  136. glaip_sdk/registry/__init__.py +55 -0
  137. glaip_sdk/registry/agent.py +164 -0
  138. glaip_sdk/registry/base.py +139 -0
  139. glaip_sdk/registry/mcp.py +253 -0
  140. glaip_sdk/registry/tool.py +445 -0
  141. glaip_sdk/rich_components.py +58 -2
  142. glaip_sdk/runner/__init__.py +76 -0
  143. glaip_sdk/runner/base.py +84 -0
  144. glaip_sdk/runner/deps.py +115 -0
  145. glaip_sdk/runner/langgraph.py +1055 -0
  146. glaip_sdk/runner/logging_config.py +77 -0
  147. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  148. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  149. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  150. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +116 -0
  151. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  152. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  153. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +242 -0
  154. glaip_sdk/schedules/__init__.py +22 -0
  155. glaip_sdk/schedules/base.py +291 -0
  156. glaip_sdk/tools/__init__.py +22 -0
  157. glaip_sdk/tools/base.py +488 -0
  158. glaip_sdk/utils/__init__.py +59 -12
  159. glaip_sdk/utils/a2a/__init__.py +34 -0
  160. glaip_sdk/utils/a2a/event_processor.py +188 -0
  161. glaip_sdk/utils/agent_config.py +8 -2
  162. glaip_sdk/utils/bundler.py +403 -0
  163. glaip_sdk/utils/client.py +111 -0
  164. glaip_sdk/utils/client_utils.py +39 -7
  165. glaip_sdk/utils/datetime_helpers.py +58 -0
  166. glaip_sdk/utils/discovery.py +78 -0
  167. glaip_sdk/utils/display.py +23 -15
  168. glaip_sdk/utils/export.py +143 -0
  169. glaip_sdk/utils/general.py +0 -33
  170. glaip_sdk/utils/import_export.py +12 -7
  171. glaip_sdk/utils/import_resolver.py +524 -0
  172. glaip_sdk/utils/instructions.py +101 -0
  173. glaip_sdk/utils/rendering/__init__.py +115 -1
  174. glaip_sdk/utils/rendering/formatting.py +5 -30
  175. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  176. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  177. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  178. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  179. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  180. glaip_sdk/utils/rendering/models.py +1 -0
  181. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  182. glaip_sdk/utils/rendering/renderer/base.py +299 -1434
  183. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  184. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  185. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  186. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  187. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  188. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  189. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  190. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  191. glaip_sdk/utils/rendering/state.py +204 -0
  192. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  193. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  194. glaip_sdk/utils/rendering/steps/format.py +176 -0
  195. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  196. glaip_sdk/utils/rendering/timing.py +36 -0
  197. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  198. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  199. glaip_sdk/utils/resource_refs.py +25 -13
  200. glaip_sdk/utils/runtime_config.py +426 -0
  201. glaip_sdk/utils/serialization.py +18 -0
  202. glaip_sdk/utils/sync.py +162 -0
  203. glaip_sdk/utils/tool_detection.py +301 -0
  204. glaip_sdk/utils/tool_storage_provider.py +140 -0
  205. glaip_sdk/utils/validation.py +16 -24
  206. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/METADATA +69 -23
  207. glaip_sdk-0.7.17.dist-info/RECORD +224 -0
  208. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.7.17.dist-info}/WHEEL +2 -1
  209. glaip_sdk-0.7.17.dist-info/entry_points.txt +2 -0
  210. glaip_sdk-0.7.17.dist-info/top_level.txt +1 -0
  211. glaip_sdk/cli/commands/agents.py +0 -1369
  212. glaip_sdk/cli/commands/mcps.py +0 -1187
  213. glaip_sdk/cli/commands/tools.py +0 -584
  214. glaip_sdk/cli/utils.py +0 -1278
  215. glaip_sdk/models.py +0 -240
  216. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  217. glaip_sdk-0.1.2.dist-info/entry_points.txt +0 -3
@@ -0,0 +1,184 @@
1
+ """Shared presenter utilities for CLI/offline transcript viewing.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from typing import Any
11
+
12
+ from rich.console import Console
13
+
14
+ from glaip_sdk.utils.rendering.layout.transcript import (
15
+ DEFAULT_TRANSCRIPT_THEME,
16
+ TranscriptGlyphs,
17
+ TranscriptSnapshot,
18
+ build_transcript_snapshot,
19
+ build_transcript_view,
20
+ )
21
+ from glaip_sdk.utils.rendering.renderer.debug import render_debug_event_stream
22
+ from glaip_sdk.utils.rendering.state import RendererState, coerce_received_at
23
+ from glaip_sdk.utils.rendering.steps import StepManager
24
+
25
+
26
+ @dataclass(slots=True)
27
+ class ViewerContext:
28
+ """Runtime context passed to transcript presenters."""
29
+
30
+ manifest_entry: dict[str, Any]
31
+ events: list[dict[str, Any]]
32
+ default_output: str
33
+ final_output: str
34
+ stream_started_at: float | None
35
+ meta: dict[str, Any]
36
+
37
+
38
+ def render_post_run_view(
39
+ console: Console,
40
+ ctx: ViewerContext,
41
+ *,
42
+ glyphs: TranscriptGlyphs | None = None,
43
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
44
+ ) -> TranscriptSnapshot:
45
+ """Render the default summary view and return the snapshot used."""
46
+ snapshot, _state = prepare_viewer_snapshot(
47
+ ctx,
48
+ glyphs=glyphs,
49
+ theme=theme,
50
+ )
51
+ render_transcript_view(console, snapshot, theme=theme)
52
+ return snapshot
53
+
54
+
55
+ def render_transcript_view(
56
+ console: Console,
57
+ snapshot: TranscriptSnapshot,
58
+ *,
59
+ theme: str = DEFAULT_TRANSCRIPT_THEME,
60
+ ) -> None:
61
+ """Render the transcript summary using a prepared snapshot."""
62
+ header, body = build_transcript_view(snapshot, theme=theme)
63
+ _print_renderables(console, header + body)
64
+
65
+
66
+ def render_transcript_events(console: Console, events: list[dict[str, Any]]) -> None:
67
+ """Pretty-print transcript events using shared debug presenter."""
68
+ if not events:
69
+ console.print("[dim]No SSE events were captured for this run.[/dim]")
70
+ console.print()
71
+ return
72
+
73
+ console.print("[bold]Transcript Events[/bold]")
74
+ console.print("[dim]────────────────────────────────────────────────────────[/dim]")
75
+
76
+ render_debug_event_stream(
77
+ events,
78
+ console,
79
+ resolve_timestamp=lambda event: coerce_received_at(event.get("received_at")),
80
+ )
81
+ console.print()
82
+
83
+
84
+ def prepare_viewer_snapshot(
85
+ ctx: ViewerContext,
86
+ *,
87
+ glyphs: TranscriptGlyphs | None,
88
+ theme: str,
89
+ ) -> tuple[TranscriptSnapshot, RendererState]:
90
+ """Build a transcript snapshot plus renderer state for reusable viewing."""
91
+ state = _build_renderer_state(ctx)
92
+ manager = _build_steps_from_events(ctx.events)
93
+ query = _extract_query_from_manifest(ctx)
94
+ merged_meta = _merge_meta(ctx)
95
+ snapshot = build_transcript_snapshot(
96
+ state,
97
+ manager,
98
+ glyphs=glyphs,
99
+ query_text=query,
100
+ meta=merged_meta,
101
+ theme=theme,
102
+ )
103
+ return snapshot, state
104
+
105
+
106
+ def _build_renderer_state(ctx: ViewerContext) -> RendererState:
107
+ state = RendererState()
108
+ state.meta = dict(ctx.meta or {})
109
+
110
+ final_text = (ctx.final_output or "").strip()
111
+ default_text = (ctx.default_output or "").strip()
112
+ if final_text:
113
+ state.final_text = final_text
114
+ elif default_text:
115
+ state.final_text = default_text
116
+ state.buffer.append(default_text)
117
+
118
+ duration = _extract_final_duration(ctx.events)
119
+ if duration:
120
+ state.final_duration_text = duration # pragma: no cover - exercised indirectly via end-to-end tests
121
+ state.events = list(ctx.events or [])
122
+ return state
123
+
124
+
125
+ def _build_steps_from_events(events: list[dict[str, Any]]) -> StepManager:
126
+ manager = StepManager()
127
+ for event in events or []:
128
+ payload = _coerce_step_event(event)
129
+ if not payload:
130
+ continue
131
+ try:
132
+ manager.apply_event(payload)
133
+ except ValueError:
134
+ continue
135
+ return manager
136
+
137
+
138
+ def _coerce_step_event(event: dict[str, Any]) -> dict[str, Any] | None:
139
+ metadata = event.get("metadata")
140
+ if not isinstance(metadata, dict):
141
+ return None
142
+ if not isinstance(metadata.get("step_id"), str):
143
+ return None
144
+ return {
145
+ "metadata": metadata,
146
+ "status": event.get("status"),
147
+ "task_state": event.get("task_state"),
148
+ "content": event.get("content"),
149
+ "task_id": event.get("task_id"),
150
+ "context_id": event.get("context_id"),
151
+ }
152
+
153
+
154
+ def _extract_final_duration(events: list[dict[str, Any]]) -> str | None:
155
+ for event in events or []:
156
+ metadata = event.get("metadata") or {}
157
+ if metadata.get("kind") != "final_response":
158
+ continue
159
+ time_value = metadata.get("time")
160
+ if isinstance(time_value, (int, float)):
161
+ return f"{float(time_value):.2f}s"
162
+ return None
163
+
164
+
165
+ def _extract_query_from_manifest(ctx: ViewerContext) -> str | None:
166
+ query = ctx.manifest_entry.get("input_message") or ctx.meta.get("input_message") or ctx.meta.get("query")
167
+ if isinstance(query, str) and query.strip():
168
+ return query.strip()
169
+ return None
170
+
171
+
172
+ def _merge_meta(ctx: ViewerContext) -> dict[str, Any]:
173
+ merged = dict(ctx.meta or {})
174
+ manifest = ctx.manifest_entry or {}
175
+ for key in ("agent_name", "agent_id", "model", "run_id", "input_message"):
176
+ if key in manifest and manifest[key] and key not in merged:
177
+ merged[key] = manifest[key]
178
+ return merged
179
+
180
+
181
+ def _print_renderables(console: Console, renderables: list[Any]) -> None:
182
+ for renderable in renderables:
183
+ console.print(renderable)
184
+ console.print()
@@ -29,6 +29,28 @@ def is_uuid(value: str) -> bool:
29
29
  return False
30
30
 
31
31
 
32
+ def _extract_id_from_item(item: Any, *, skip_missing: bool = False) -> str | None:
33
+ """Extract ID from a single item.
34
+
35
+ Args:
36
+ item: Item that may be a string, object with .id, or dict with "id" key.
37
+ skip_missing: If True, return None for items without IDs. If False, convert to string.
38
+
39
+ Returns:
40
+ Extracted ID as string, or None if skip_missing=True and no ID found.
41
+ """
42
+ if isinstance(item, str):
43
+ return item
44
+ if hasattr(item, "id"):
45
+ return str(item.id)
46
+ if isinstance(item, dict) and "id" in item:
47
+ return str(item["id"])
48
+ if skip_missing:
49
+ return None
50
+ # Fallback: convert to string
51
+ return str(item)
52
+
53
+
32
54
  def extract_ids(items: list[str | Any] | None) -> list[str]:
33
55
  """Extract IDs from a list of objects or strings.
34
56
 
@@ -49,19 +71,9 @@ def extract_ids(items: list[str | Any] | None) -> list[str]:
49
71
  if not items:
50
72
  return []
51
73
 
52
- ids = []
53
- for item in items:
54
- if isinstance(item, str):
55
- ids.append(item)
56
- elif hasattr(item, "id"):
57
- ids.append(str(item.id))
58
- elif isinstance(item, dict) and "id" in item:
59
- ids.append(str(item["id"]))
60
- else:
61
- # Fallback: convert to string
62
- ids.append(str(item))
63
-
64
- return ids
74
+ # Extract IDs from all items, converting non-ID items to strings
75
+ extracted_ids = [_extract_id_from_item(item, skip_missing=False) for item in items]
76
+ return [id_val for id_val in extracted_ids if id_val is not None]
65
77
 
66
78
 
67
79
  def extract_names(items: list[str | Any] | None) -> list[str]:
@@ -0,0 +1,426 @@
1
+ """Runtime configuration helpers for agent execution.
2
+
3
+ Provides utilities for normalizing runtime_config keys from various input types
4
+ (SDK objects, UUIDs, names) to stable platform IDs.
5
+
6
+ Authors:
7
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+ from collections.abc import Mapping
14
+
15
+ from glaip_sdk.utils.resource_refs import is_uuid
16
+ from gllm_core.utils import LoggerManager
17
+
18
+ if TYPE_CHECKING:
19
+ from glaip_sdk.agents import Agent
20
+ from glaip_sdk.mcps import MCP
21
+ from glaip_sdk.registry import AgentRegistry, MCPRegistry, ToolRegistry
22
+ from glaip_sdk.tools import Tool
23
+
24
+ # Type alias for config key inputs (only used in type hints)
25
+ ConfigKeyInput = str | Agent | Tool | MCP | type[Agent] | type[Tool] | type[MCP]
26
+
27
+ # Type alias for registry types
28
+ Registry = ToolRegistry | MCPRegistry | AgentRegistry
29
+
30
+ # Type alias for runtime config dict shape after normalization
31
+ # Top-level keys include:
32
+ # - "tool_configs", "mcp_configs", "agent_config"
33
+ # - Agent IDs for agent-specific overrides
34
+ # Values are nested dictionaries whose contents depend on the section.
35
+ RuntimeConfigDict = dict[str, dict[str, object]]
36
+
37
+ logger = LoggerManager().get_logger(__name__)
38
+
39
+ # Config fields that need key normalization (maps field name to registry type)
40
+ _NORMALIZABLE_FIELDS = {
41
+ "tool_configs": "tool",
42
+ "mcp_configs": "mcp",
43
+ }
44
+
45
+ # Config fields that are preserved as-is (no normalization needed)
46
+ _PASSTHROUGH_FIELDS = {"agent_config"}
47
+
48
+
49
+ def _extract_id_from_key(key: ConfigKeyInput) -> str | None:
50
+ """Extract ID directly from key if available.
51
+
52
+ Args:
53
+ key: The config key to extract ID from.
54
+
55
+ Returns:
56
+ The ID string if available, None otherwise.
57
+ """
58
+ if isinstance(key, str) and is_uuid(key):
59
+ return key
60
+ if hasattr(key, "id") and key.id is not None:
61
+ return key.id
62
+ return None
63
+
64
+
65
+ def _resolve_via_registry(key: ConfigKeyInput, registry: Registry | None) -> str | None:
66
+ """Attempt to resolve key via registry.
67
+
68
+ Args:
69
+ key: The config key to resolve.
70
+ registry: Registry to use for resolution.
71
+
72
+ Returns:
73
+ The resolved ID string if successful, None otherwise.
74
+ """
75
+ if registry is None:
76
+ return None
77
+
78
+ try:
79
+ return registry.resolve(key).id
80
+ except (KeyError, ValueError, AttributeError) as exc:
81
+ logger.debug("Failed to resolve key via registry: %r", key, exc_info=exc)
82
+ return None
83
+ except Exception as exc: # pragma: no cover - unexpected failures
84
+ logger.warning(
85
+ "Unexpected error resolving key via registry for %r: %s",
86
+ key,
87
+ exc,
88
+ exc_info=True,
89
+ )
90
+ return None
91
+
92
+
93
+ def _resolve_config_key(
94
+ key: ConfigKeyInput,
95
+ registry: Registry | None,
96
+ *,
97
+ missing_registry_message: str,
98
+ unresolved_message: str,
99
+ ) -> str:
100
+ """Resolve a config key using a registry when needed.
101
+
102
+ For non-ID keys this always requires a registry; callers customize the
103
+ error messages for missing registries vs unresolved keys.
104
+ """
105
+ # Allow direct UUID / object.id without needing a registry
106
+ if (extracted_id := _extract_id_from_key(key)) is not None:
107
+ return extracted_id
108
+
109
+ # For non-ID keys we always require a registry
110
+ if registry is None:
111
+ raise ValueError(missing_registry_message.format(key=key))
112
+
113
+ if (resolved_id := _resolve_via_registry(key, registry)) is not None:
114
+ return resolved_id
115
+
116
+ raise ValueError(unresolved_message.format(key=key))
117
+
118
+
119
+ def _normalize_section_keys(
120
+ config_section: dict[ConfigKeyInput, dict[str, object]],
121
+ registry: Registry | None,
122
+ ) -> dict[str, dict[str, object]]:
123
+ """Normalize keys in a single config section (e.g. tool_configs).
124
+
125
+ Converts ConfigKeyInput keys (SDK objects, names, classes) to stable IDs
126
+ using the provided registry.
127
+
128
+ Example:
129
+ Input: {ToolClass: {"param": "value"}, "tool-name": {"x": 1}}
130
+ Output: {"uuid-1": {"param": "value"}, "uuid-2": {"x": 1}}
131
+
132
+ Args:
133
+ config_section: The config section dict to normalize.
134
+ registry: Registry to use for resolving keys.
135
+
136
+ Returns:
137
+ Normalized config section with all keys converted to IDs.
138
+ """
139
+ normalized: dict[str, dict[str, object]] = {}
140
+ for key, value in config_section.items():
141
+ resolved_id = _resolve_config_key(
142
+ key,
143
+ registry,
144
+ missing_registry_message="Unable to resolve runtime_config key via registry: {key!r}",
145
+ unresolved_message="Unable to resolve runtime_config key via registry: {key!r}",
146
+ )
147
+ normalized[resolved_id] = value
148
+ return normalized
149
+
150
+
151
+ def _is_agent_specific_key(key: object) -> bool:
152
+ """Check if a key represents an agent-specific override.
153
+
154
+ Agent-specific keys are:
155
+ - Agent instances (from glaip_sdk.agents.Agent)
156
+ - UUID strings (agent IDs)
157
+ - Non-reserved string names (resolved via agent_registry)
158
+
159
+ NOT agent-specific:
160
+ - Known config field names (tool_configs, mcp_configs, agent_config)
161
+ - Tool or MCP objects (these are only valid inside *_configs sections)
162
+
163
+ Args:
164
+ key: The key to check.
165
+
166
+ Returns:
167
+ True if the key could be an agent-specific override.
168
+ """
169
+ from glaip_sdk.agents import Agent # noqa: PLC0415
170
+
171
+ # Agent instance
172
+ if isinstance(key, Agent):
173
+ return True
174
+
175
+ # Non-string keys that are not Agent instances are not agent-specific
176
+ if not isinstance(key, str):
177
+ return False
178
+
179
+ # Known config field names are not agent-specific
180
+ if key in _NORMALIZABLE_FIELDS or key in _PASSTHROUGH_FIELDS:
181
+ return False
182
+
183
+ # Any other string key is treated as an agent reference (ID or name)
184
+ return True
185
+
186
+
187
+ def _normalize_standard_config(
188
+ config: dict[str, object],
189
+ tool_registry: ToolRegistry | None,
190
+ mcp_registry: MCPRegistry | None,
191
+ context: str = "",
192
+ ) -> dict[str, object]:
193
+ """Normalize a standard config dict with tool_configs, mcp_configs, agent_config sections.
194
+
195
+ Used for both global runtime_config and agent-specific nested configs.
196
+ Delegates key normalization to _normalize_section_keys for each section.
197
+
198
+ Example:
199
+ Input: {"tool_configs": {ToolClass: {...}}, "agent_config": {...}}
200
+ Output: {"tool_configs": {"tool-uuid": {...}}, "agent_config": {...}}
201
+
202
+ Args:
203
+ config: The config dict to normalize.
204
+ tool_registry: Registry for resolving tool keys.
205
+ mcp_registry: Registry for resolving MCP keys.
206
+ context: Context string for warning messages.
207
+
208
+ Returns:
209
+ Normalized config dict.
210
+ """
211
+ registries: dict[str, Registry | None] = {
212
+ "tool": tool_registry,
213
+ "mcp": mcp_registry,
214
+ }
215
+
216
+ result: dict[str, object] = {}
217
+
218
+ for field, value in config.items():
219
+ if field in _NORMALIZABLE_FIELDS and isinstance(value, dict):
220
+ registry_type = _NORMALIZABLE_FIELDS[field]
221
+ result[field] = _normalize_section_keys(value, registries.get(registry_type))
222
+ elif field in _PASSTHROUGH_FIELDS:
223
+ result[field] = value
224
+ else:
225
+ logger.warning("Unknown field '%s' in %s, ignoring", field, context or "config")
226
+
227
+ return result
228
+
229
+
230
+ def normalize_runtime_config_keys(
231
+ runtime_config: RuntimeConfigDict | None,
232
+ tool_registry: ToolRegistry | None,
233
+ mcp_registry: MCPRegistry | None,
234
+ agent_registry: AgentRegistry | None,
235
+ ) -> RuntimeConfigDict | None:
236
+ """Normalize runtime_config keys from various input types to stable IDs.
237
+
238
+ Handles both global configs and agent-specific overrides:
239
+ - Global: tool_configs, mcp_configs, agent_config
240
+ - Agent-specific: keyed by Agent object, agent UUID, or agent name string
241
+
242
+ Example:
243
+ Input:
244
+ {
245
+ "tool_configs": {ToolClass: {"param": "val"}},
246
+ "agent_config": {"planning": True},
247
+ AgentClass: {"mcp_configs": {MCPClass: {...}}}
248
+ }
249
+ Output:
250
+ {
251
+ "tool_configs": {"tool-uuid": {"param": "val"}},
252
+ "agent_config": {"planning": True},
253
+ "agent-uuid": {"mcp_configs": {"mcp-uuid": {...}}}
254
+ }
255
+
256
+ Converts keys from:
257
+ - SDK objects → their .id attribute
258
+ - UUID strings → passed through
259
+ - Names → resolved via appropriate registry
260
+
261
+ Args:
262
+ runtime_config: The runtime configuration dict to normalize.
263
+ tool_registry: Registry for resolving tool keys.
264
+ mcp_registry: Registry for resolving MCP keys.
265
+ agent_registry: Registry for resolving agent keys.
266
+
267
+ Returns:
268
+ Normalized runtime_config with all keys converted to IDs, or None if input is None.
269
+ """
270
+ if runtime_config is None:
271
+ return None
272
+
273
+ if not runtime_config:
274
+ return {}
275
+
276
+ registries: dict[str, Registry | None] = {
277
+ "tool": tool_registry,
278
+ "mcp": mcp_registry,
279
+ }
280
+
281
+ result: dict[str, object] = {}
282
+
283
+ for field, value in runtime_config.items():
284
+ if field in _NORMALIZABLE_FIELDS and isinstance(value, dict):
285
+ registry_type = _NORMALIZABLE_FIELDS[field]
286
+ result[field] = _normalize_section_keys(value, registries.get(registry_type))
287
+ elif field in _PASSTHROUGH_FIELDS:
288
+ result[field] = value
289
+ elif _is_agent_specific_key(field) and isinstance(value, dict):
290
+ agent_id = _resolve_config_key(
291
+ field,
292
+ agent_registry,
293
+ missing_registry_message=(
294
+ "Agent-specific runtime_config provided but no agent_registry is available to resolve key: {key!r}"
295
+ ),
296
+ unresolved_message="Unable to resolve agent-specific runtime_config key: {key!r}",
297
+ )
298
+ result[agent_id] = _normalize_standard_config(
299
+ value,
300
+ tool_registry,
301
+ mcp_registry,
302
+ context=f"agent '{agent_id}'",
303
+ )
304
+ else:
305
+ logger.warning("Unknown field '%s' in runtime_config, ignoring", field)
306
+
307
+ return result
308
+
309
+
310
+ # =============================================================================
311
+ # LOCAL MODE UTILITIES
312
+ # =============================================================================
313
+ # The functions below are for local execution mode where resources are NOT
314
+ # deployed and have no UUIDs. They resolve keys to names (not IDs).
315
+ # =============================================================================
316
+
317
+
318
+ def _get_name_from_class(cls: type) -> str:
319
+ """Extract name from a class, handling Pydantic models and @property descriptors.
320
+
321
+ Args:
322
+ cls: The class to extract name from.
323
+
324
+ Returns:
325
+ The resolved name string.
326
+ """
327
+ # Try class-level name attribute first, but guard against @property descriptors
328
+ # When a class has @property name, getattr returns the property object, not a string
329
+ class_name = getattr(cls, "name", None)
330
+ if isinstance(class_name, str) and class_name:
331
+ return class_name
332
+
333
+ # For Pydantic models, check model_fields for default value
334
+ model_fields = getattr(cls, "model_fields", None)
335
+ if model_fields and "name" in model_fields:
336
+ field_info = model_fields["name"]
337
+ default = getattr(field_info, "default", None)
338
+ if default and isinstance(default, str):
339
+ return default
340
+
341
+ # Fallback to class __name__
342
+ return cls.__name__
343
+
344
+
345
+ def get_name_from_key(key: object) -> str | None:
346
+ """Resolve config key to name for local mode (no registry needed).
347
+
348
+ Supports instances, classes, and string names. UUID strings are not
349
+ supported in local mode and return None with a warning.
350
+
351
+ Args:
352
+ key: Tool, MCP, or Agent instance/class/string.
353
+
354
+ Returns:
355
+ The resolved name string, or None if UUID (not applicable locally).
356
+
357
+ Raises:
358
+ ValueError: If the key cannot be resolved to a valid name.
359
+ """
360
+ # Class type (not instance) - must check BEFORE hasattr("name")
361
+ # because classes with @property name will have hasattr return True
362
+ # but getattr returns the property descriptor, not a string
363
+ if isinstance(key, type):
364
+ return _get_name_from_class(key)
365
+
366
+ # String key - check early to avoid attribute access
367
+ if isinstance(key, str):
368
+ if is_uuid(key):
369
+ logger.warning("UUID '%s' not supported in local mode, skipping", key)
370
+ return None
371
+ return key
372
+
373
+ # Instance with name attribute
374
+ if hasattr(key, "name"):
375
+ name = getattr(key, "name", None)
376
+ # Guard against @property that returns non-string (e.g., descriptor)
377
+ if isinstance(name, str) and name:
378
+ return name
379
+
380
+ raise ValueError(f"Unable to resolve config key: {key!r}")
381
+
382
+
383
+ def normalize_local_config_keys(config: Mapping[object, object]) -> dict[str, object]:
384
+ """Normalize all keys in a config dict to names for local mode.
385
+
386
+ Converts instance/class/string keys to string names without using
387
+ registry. UUID keys are skipped with a warning.
388
+
389
+ Args:
390
+ config: Dict/Mapping with instance/class/string keys and any values.
391
+
392
+ Returns:
393
+ Dict with string name keys only. UUID keys are omitted.
394
+ """
395
+ if not config:
396
+ return {}
397
+
398
+ result: dict[str, object] = {}
399
+ for key, value in config.items():
400
+ name = get_name_from_key(key)
401
+ if name is not None:
402
+ result[name] = value
403
+ return result
404
+
405
+
406
+ def merge_configs(*configs: dict | None) -> dict:
407
+ """Merge multiple config dicts with priority ordering.
408
+
409
+ Later configs override earlier ones for the same key. None configs
410
+ are skipped gracefully.
411
+
412
+ Args:
413
+ *configs: Config dicts in priority order (lowest priority first).
414
+
415
+ Returns:
416
+ Merged config dict with later values overriding earlier ones.
417
+
418
+ Example:
419
+ >>> merge_configs({"a": 1}, {"a": 2, "b": 3})
420
+ {"a": 2, "b": 3}
421
+ """
422
+ result: dict = {}
423
+ for config in configs:
424
+ if config:
425
+ result.update(config)
426
+ return result
@@ -88,9 +88,20 @@ def write_yaml(file_path: Path, data: dict[str, Any]) -> None:
88
88
 
89
89
  # Custom YAML dumper for user-friendly instruction formatting
90
90
  class LiteralString(str):
91
+ """String subclass for YAML literal block scalar formatting."""
92
+
91
93
  pass
92
94
 
93
95
  def literal_string_representer(dumper: yaml.Dumper, data: "LiteralString") -> yaml.nodes.Node:
96
+ """YAML representer for LiteralString to use literal block scalar style.
97
+
98
+ Args:
99
+ dumper: YAML dumper instance.
100
+ data: LiteralString instance to represent.
101
+
102
+ Returns:
103
+ YAML node with literal block scalar style for multiline strings.
104
+ """
94
105
  # Use literal block scalar (|) for multiline strings to preserve formatting
95
106
  if "\n" in data:
96
107
  return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
@@ -239,6 +250,11 @@ def _iter_public_attribute_names(resource: Any) -> Iterable[str]:
239
250
  names: list[str] = []
240
251
 
241
252
  def _collect(candidates: Iterable[str] | None) -> None:
253
+ """Collect unique candidate attribute names.
254
+
255
+ Args:
256
+ candidates: Iterable of candidate attribute names.
257
+ """
242
258
  for candidate in candidates or ():
243
259
  if candidate not in seen:
244
260
  seen.add(candidate)
@@ -287,6 +303,7 @@ def _collect_from_dir(resource: Any, collect_func: Callable[[Iterable[str]], Non
287
303
 
288
304
 
289
305
  def _safe_getattr(resource: Any, name: str) -> Any:
306
+ """Return getattr(resource, name) but swallow any exception and return None."""
290
307
  try:
291
308
  return getattr(resource, name)
292
309
  except Exception:
@@ -294,6 +311,7 @@ def _safe_getattr(resource: Any, name: str) -> Any:
294
311
 
295
312
 
296
313
  def _should_include_attribute(key: str, value: Any) -> bool:
314
+ """Return True when an attribute should be serialized."""
297
315
  if key in _EXCLUDED_ATTRS or key in _EXCLUDED_NAMES:
298
316
  return False
299
317
  if key.startswith("_"):