glaip-sdk 0.6.5b6__py3-none-any.whl → 0.7.12__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 (127) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +217 -42
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  8. glaip_sdk/cli/commands/agents/_common.py +561 -0
  9. glaip_sdk/cli/commands/agents/create.py +151 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +15 -12
  17. glaip_sdk/cli/commands/configure.py +2 -3
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/core/output.py +12 -7
  46. glaip_sdk/cli/entrypoint.py +20 -0
  47. glaip_sdk/cli/main.py +127 -39
  48. glaip_sdk/cli/pager.py +3 -3
  49. glaip_sdk/cli/resolution.py +2 -1
  50. glaip_sdk/cli/slash/accounts_controller.py +112 -32
  51. glaip_sdk/cli/slash/agent_session.py +5 -2
  52. glaip_sdk/cli/slash/prompt.py +11 -0
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +369 -23
  55. glaip_sdk/cli/slash/tui/__init__.py +26 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +79 -5
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1027 -88
  58. glaip_sdk/cli/slash/tui/clipboard.py +195 -0
  59. glaip_sdk/cli/slash/tui/context.py +87 -0
  60. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  61. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  62. glaip_sdk/cli/slash/tui/layouts/harlequin.py +160 -0
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +119 -12
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +374 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +5 -3
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +50 -8
  78. glaip_sdk/client/hitl.py +136 -0
  79. glaip_sdk/client/main.py +7 -1
  80. glaip_sdk/client/mcps.py +44 -13
  81. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  82. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +22 -47
  83. glaip_sdk/client/payloads/agent/responses.py +43 -0
  84. glaip_sdk/client/run_rendering.py +414 -3
  85. glaip_sdk/client/schedules.py +439 -0
  86. glaip_sdk/client/tools.py +57 -26
  87. glaip_sdk/guardrails/__init__.py +80 -0
  88. glaip_sdk/guardrails/serializer.py +89 -0
  89. glaip_sdk/hitl/__init__.py +48 -0
  90. glaip_sdk/hitl/base.py +64 -0
  91. glaip_sdk/hitl/callback.py +43 -0
  92. glaip_sdk/hitl/local.py +121 -0
  93. glaip_sdk/hitl/remote.py +523 -0
  94. glaip_sdk/models/__init__.py +17 -0
  95. glaip_sdk/models/agent_runs.py +2 -1
  96. glaip_sdk/models/schedule.py +224 -0
  97. glaip_sdk/payload_schemas/agent.py +1 -0
  98. glaip_sdk/payload_schemas/guardrails.py +34 -0
  99. glaip_sdk/registry/tool.py +273 -59
  100. glaip_sdk/runner/__init__.py +20 -3
  101. glaip_sdk/runner/deps.py +5 -8
  102. glaip_sdk/runner/langgraph.py +318 -42
  103. glaip_sdk/runner/logging_config.py +77 -0
  104. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +104 -5
  105. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +72 -7
  106. glaip_sdk/schedules/__init__.py +22 -0
  107. glaip_sdk/schedules/base.py +291 -0
  108. glaip_sdk/tools/base.py +67 -14
  109. glaip_sdk/utils/__init__.py +1 -0
  110. glaip_sdk/utils/bundler.py +138 -2
  111. glaip_sdk/utils/import_resolver.py +43 -11
  112. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  113. glaip_sdk/utils/runtime_config.py +15 -12
  114. glaip_sdk/utils/sync.py +31 -11
  115. glaip_sdk/utils/tool_detection.py +274 -6
  116. glaip_sdk/utils/tool_storage_provider.py +140 -0
  117. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/METADATA +49 -37
  118. glaip_sdk-0.7.12.dist-info/RECORD +219 -0
  119. {glaip_sdk-0.6.5b6.dist-info → glaip_sdk-0.7.12.dist-info}/WHEEL +2 -1
  120. glaip_sdk-0.7.12.dist-info/entry_points.txt +2 -0
  121. glaip_sdk-0.7.12.dist-info/top_level.txt +1 -0
  122. glaip_sdk/cli/commands/agents.py +0 -1509
  123. glaip_sdk/cli/commands/mcps.py +0 -1356
  124. glaip_sdk/cli/commands/tools.py +0 -576
  125. glaip_sdk/cli/utils.py +0 -263
  126. glaip_sdk-0.6.5b6.dist-info/RECORD +0 -159
  127. glaip_sdk-0.6.5b6.dist-info/entry_points.txt +0 -3
@@ -19,18 +19,19 @@ Example:
19
19
  >>> result = runner.run(agent, "Hello!")
20
20
  """
21
21
 
22
+ from typing import TYPE_CHECKING, Any
23
+
22
24
  from glaip_sdk.runner.deps import (
23
25
  LOCAL_RUNTIME_AVAILABLE,
24
26
  check_local_runtime_available,
25
27
  get_local_runtime_missing_message,
26
28
  )
27
- from glaip_sdk.runner.langgraph import LangGraphRunner
28
29
 
29
30
  # Default runner instance
30
- _default_runner: LangGraphRunner | None = None
31
+ _default_runner: Any | None = None
31
32
 
32
33
 
33
- def get_default_runner() -> LangGraphRunner:
34
+ def get_default_runner() -> Any:
34
35
  """Get the default runner instance for local agent execution.
35
36
 
36
37
  Returns:
@@ -45,11 +46,17 @@ def get_default_runner() -> LangGraphRunner:
45
46
  raise RuntimeError(get_local_runtime_missing_message())
46
47
 
47
48
  if _default_runner is None:
49
+ # Lazy import to avoid requiring aip-agents when runner is not used
50
+ from glaip_sdk.runner.langgraph import LangGraphRunner # noqa: PLC0415
51
+
48
52
  _default_runner = LangGraphRunner()
49
53
 
50
54
  return _default_runner
51
55
 
52
56
 
57
+ if TYPE_CHECKING:
58
+ from glaip_sdk.runner.langgraph import LangGraphRunner
59
+
53
60
  __all__ = [
54
61
  "LOCAL_RUNTIME_AVAILABLE",
55
62
  "LangGraphRunner",
@@ -57,3 +64,13 @@ __all__ = [
57
64
  "get_default_runner",
58
65
  "get_local_runtime_missing_message",
59
66
  ]
67
+
68
+
69
+ def __getattr__(name: str) -> Any:
70
+ """Lazy import for LangGraphRunner to avoid requiring aip-agents when not used."""
71
+ if name == "LangGraphRunner":
72
+ from glaip_sdk.runner.langgraph import LangGraphRunner # noqa: PLC0415
73
+
74
+ globals()["LangGraphRunner"] = LangGraphRunner
75
+ return LangGraphRunner
76
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
glaip_sdk/runner/deps.py CHANGED
@@ -15,6 +15,8 @@ Example:
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import importlib.util
19
+
18
20
  from gllm_core.utils import LoggerManager
19
21
 
20
22
  logger = LoggerManager().get_logger(__name__)
@@ -24,17 +26,12 @@ _local_runtime_available: bool | None = None
24
26
 
25
27
 
26
28
  def _probe_aip_agents_import() -> bool:
27
- """Attempt to import aip_agents and return success status.
29
+ """Check if aip_agents is available without importing it.
28
30
 
29
31
  Returns:
30
- True if aip_agents can be imported successfully, False otherwise.
32
+ True if aip_agents appears importable, False otherwise.
31
33
  """
32
- try:
33
- import aip_agents # noqa: F401, PLC0415
34
-
35
- return True
36
- except ImportError:
37
- return False
34
+ return importlib.util.find_spec("aip_agents") is not None
38
35
 
39
36
 
40
37
  def check_local_runtime_available() -> bool:
@@ -18,33 +18,102 @@ Example:
18
18
  from __future__ import annotations
19
19
 
20
20
  import asyncio
21
+ import inspect
22
+ import logging
21
23
  from dataclasses import dataclass
22
24
  from typing import TYPE_CHECKING, Any
23
25
 
26
+ from gllm_core.utils import LoggerManager
27
+
28
+ from glaip_sdk.client.run_rendering import AgentRunRenderingManager
29
+ from glaip_sdk.hitl import PauseResumeCallback
24
30
  from glaip_sdk.runner.base import BaseRunner
25
31
  from glaip_sdk.runner.deps import (
26
32
  check_local_runtime_available,
27
33
  get_local_runtime_missing_message,
28
34
  )
29
- from glaip_sdk.utils.a2a import A2AEventStreamProcessor
30
- from gllm_core.utils import LoggerManager
35
+ from glaip_sdk.utils.tool_storage_provider import build_tool_output_manager
31
36
 
32
37
  if TYPE_CHECKING:
38
+ from langchain_core.messages import BaseMessage
39
+
33
40
  from glaip_sdk.agents.base import Agent
34
41
 
42
+
43
+ _AIP_LOGS_SWALLOWED = False
44
+
45
+
46
+ def _swallow_aip_logs(level: int = logging.ERROR) -> None:
47
+ """Consume noisy AIPAgents logs once (opt-in via runner flag)."""
48
+ global _AIP_LOGS_SWALLOWED
49
+ if _AIP_LOGS_SWALLOWED:
50
+ return
51
+ prefixes = ("aip_agents.",)
52
+
53
+ def _silence(name: str) -> None:
54
+ lg = logging.getLogger(name)
55
+ lg.handlers = [logging.NullHandler()]
56
+ lg.propagate = False
57
+ lg.setLevel(level)
58
+
59
+ # Silence any already-registered loggers under the given prefixes
60
+ for logger_name in logging.root.manager.loggerDict:
61
+ if any(logger_name.startswith(prefix) for prefix in prefixes):
62
+ _silence(logger_name)
63
+
64
+ # Also set the base prefix loggers so future children inherit silence
65
+ for prefix in prefixes:
66
+ _silence(prefix.rstrip("."))
67
+ _AIP_LOGS_SWALLOWED = True
68
+
69
+
35
70
  logger = LoggerManager().get_logger(__name__)
36
71
 
37
- # Default A2A event processor
38
- _event_processor = A2AEventStreamProcessor()
72
+
73
+ def _convert_chat_history_to_messages(
74
+ chat_history: list[dict[str, str]] | None,
75
+ ) -> list[BaseMessage]:
76
+ """Convert chat history dicts to LangChain messages.
77
+
78
+ Args:
79
+ chat_history: List of dicts with "role" and "content" keys.
80
+ Supported roles: "user"/"human", "assistant"/"ai", "system".
81
+
82
+ Returns:
83
+ List of LangChain BaseMessage instances.
84
+ """
85
+ if not chat_history:
86
+ return []
87
+
88
+ from langchain_core.messages import AIMessage, HumanMessage, SystemMessage # noqa: PLC0415
89
+
90
+ messages: list[BaseMessage] = []
91
+ for msg in chat_history:
92
+ role = msg.get("role", "").lower()
93
+ content = msg.get("content", "")
94
+
95
+ if role in ("user", "human"):
96
+ messages.append(HumanMessage(content=content))
97
+ elif role in ("assistant", "ai"):
98
+ messages.append(AIMessage(content=content))
99
+ elif role == "system":
100
+ messages.append(SystemMessage(content=content))
101
+ else:
102
+ # Default to human message for unknown roles
103
+ logger.warning("Unknown chat history role '%s', treating as user message", role)
104
+ messages.append(HumanMessage(content=content))
105
+
106
+ return messages
39
107
 
40
108
 
41
109
  @dataclass(frozen=True, slots=True)
42
110
  class LangGraphRunner(BaseRunner):
43
111
  """Runner implementation using aip-agents LangGraphReactAgent.
44
112
 
45
- MVP scope:
46
- - Execute via `LangGraphReactAgent.arun_a2a_stream()`
47
- - Extract and return final text from the emitted `final_response` event
113
+ Current behavior:
114
+ - Execute via `LangGraphReactAgent.arun_sse_stream()` (normalized SSE-compatible stream)
115
+ - Route all events through `AgentRunRenderingManager.async_process_stream_events`
116
+ for unified rendering between local and remote agents
48
117
 
49
118
  Attributes:
50
119
  default_model: Model name to use when agent.model is not set.
@@ -58,8 +127,10 @@ class LangGraphRunner(BaseRunner):
58
127
  agent: Agent,
59
128
  message: str,
60
129
  verbose: bool = False,
61
- runtime_config: dict[str, Any] | None = None, # noqa: ARG002 - Used in PR-04+
62
- chat_history: (list[dict[str, str]] | None) = None, # noqa: ARG002 - Used in PR-03
130
+ runtime_config: dict[str, Any] | None = None,
131
+ chat_history: list[dict[str, str]] | None = None,
132
+ *,
133
+ swallow_aip_logs: bool = True,
63
134
  **kwargs: Any,
64
135
  ) -> str:
65
136
  """Execute agent synchronously and return final response text.
@@ -72,7 +143,11 @@ class LangGraphRunner(BaseRunner):
72
143
  runtime_config: Optional runtime configuration for tools, MCPs, etc.
73
144
  Defaults to None. (Implemented in PR-04+)
74
145
  chat_history: Optional list of prior conversation messages.
75
- Defaults to None. (Implemented in PR-03)
146
+ Each message is a dict with "role" and "content" keys.
147
+ Defaults to None.
148
+ swallow_aip_logs: When True (default), silence noisy logs from aip-agents,
149
+ gllm_inference, OpenAILMInvoker, and httpx. Set to False to honor user
150
+ logging configuration.
76
151
  **kwargs: Additional keyword arguments passed to the backend.
77
152
 
78
153
  Returns:
@@ -85,23 +160,37 @@ class LangGraphRunner(BaseRunner):
85
160
  if not check_local_runtime_available():
86
161
  raise RuntimeError(get_local_runtime_missing_message())
87
162
 
88
- return asyncio.run(
89
- self._arun_internal(
90
- agent=agent,
91
- message=message,
92
- verbose=verbose,
93
- runtime_config=runtime_config,
94
- **kwargs,
163
+ try:
164
+ asyncio.get_running_loop()
165
+ except RuntimeError:
166
+ pass
167
+ else:
168
+ raise RuntimeError(
169
+ "LangGraphRunner.run() cannot be called from a running event loop. "
170
+ "Use 'await LangGraphRunner.arun(...)' instead."
95
171
  )
172
+
173
+ coro = self._arun_internal(
174
+ agent=agent,
175
+ message=message,
176
+ verbose=verbose,
177
+ runtime_config=runtime_config,
178
+ chat_history=chat_history,
179
+ swallow_aip_logs=swallow_aip_logs,
180
+ **kwargs,
96
181
  )
97
182
 
183
+ return asyncio.run(coro)
184
+
98
185
  async def arun(
99
186
  self,
100
187
  agent: Agent,
101
188
  message: str,
102
189
  verbose: bool = False,
103
190
  runtime_config: dict[str, Any] | None = None,
104
- chat_history: (list[dict[str, str]] | None) = None, # noqa: ARG002 - Used in PR-03
191
+ chat_history: list[dict[str, str]] | None = None,
192
+ *,
193
+ swallow_aip_logs: bool = True,
105
194
  **kwargs: Any,
106
195
  ) -> str:
107
196
  """Execute agent asynchronously and return final response text.
@@ -114,7 +203,9 @@ class LangGraphRunner(BaseRunner):
114
203
  runtime_config: Optional runtime configuration for tools, MCPs, etc.
115
204
  Defaults to None. (Implemented in PR-04+)
116
205
  chat_history: Optional list of prior conversation messages.
117
- Defaults to None. (Implemented in PR-03)
206
+ Each message is a dict with "role" and "content" keys.
207
+ Defaults to None.
208
+ swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
118
209
  **kwargs: Additional keyword arguments passed to the backend.
119
210
 
120
211
  Returns:
@@ -128,6 +219,8 @@ class LangGraphRunner(BaseRunner):
128
219
  message=message,
129
220
  verbose=verbose,
130
221
  runtime_config=runtime_config,
222
+ chat_history=chat_history,
223
+ swallow_aip_logs=swallow_aip_logs,
131
224
  **kwargs,
132
225
  )
133
226
 
@@ -137,6 +230,9 @@ class LangGraphRunner(BaseRunner):
137
230
  message: str,
138
231
  verbose: bool = False,
139
232
  runtime_config: dict[str, Any] | None = None,
233
+ chat_history: list[dict[str, str]] | None = None,
234
+ *,
235
+ swallow_aip_logs: bool = True,
140
236
  **kwargs: Any,
141
237
  ) -> str:
142
238
  """Internal async implementation of agent execution.
@@ -146,28 +242,83 @@ class LangGraphRunner(BaseRunner):
146
242
  message: The user message to send to the agent.
147
243
  verbose: If True, emit debug trace output during execution.
148
244
  runtime_config: Optional runtime configuration for tools, MCPs, etc.
245
+ chat_history: Optional list of prior conversation messages.
246
+ swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
149
247
  **kwargs: Additional keyword arguments passed to the backend.
150
248
 
151
249
  Returns:
152
250
  The final response text from the agent.
153
251
  """
252
+ # Optionally swallow noisy AIPAgents logs
253
+ if swallow_aip_logs:
254
+ _swallow_aip_logs()
255
+
256
+ # POC/MVP: Create pause/resume callback for interactive HITL input
257
+ pause_resume_callback = PauseResumeCallback()
258
+
154
259
  # Build the local LangGraphReactAgent from the glaip_sdk Agent
155
- local_agent = self.build_langgraph_agent(agent, runtime_config=runtime_config)
260
+ local_agent = self.build_langgraph_agent(
261
+ agent, runtime_config=runtime_config, pause_resume_callback=pause_resume_callback
262
+ )
156
263
 
157
- # Collect A2AEvents from the stream and extract final response
158
- events: list[dict[str, Any]] = []
264
+ # Convert chat history to LangChain messages for the agent
265
+ langchain_messages = _convert_chat_history_to_messages(chat_history)
266
+ if langchain_messages:
267
+ kwargs["messages"] = langchain_messages
268
+ logger.debug(
269
+ "Passing %d chat history messages to agent '%s'",
270
+ len(langchain_messages),
271
+ agent.name,
272
+ )
273
+
274
+ # Use shared render manager for unified processing
275
+ render_manager = AgentRunRenderingManager(logger)
276
+ renderer = render_manager.create_renderer(kwargs.get("renderer"), verbose=verbose)
277
+
278
+ # POC/MVP: Set renderer on callback so LocalPromptHandler can pause/resume Live
279
+ pause_resume_callback.set_renderer(renderer)
280
+
281
+ meta = render_manager.build_initial_metadata(agent.name, message, kwargs)
282
+ render_manager.start_renderer(renderer, meta)
283
+
284
+ try:
285
+ # Use shared async stream processor for unified event handling
286
+ (
287
+ final_text,
288
+ stats_usage,
289
+ started_monotonic,
290
+ finished_monotonic,
291
+ ) = await render_manager.async_process_stream_events(
292
+ local_agent.arun_sse_stream(message, **kwargs),
293
+ renderer,
294
+ meta,
295
+ skip_final_render=True,
296
+ )
297
+ except KeyboardInterrupt:
298
+ try:
299
+ renderer.close()
300
+ finally:
301
+ raise
302
+ except Exception:
303
+ try:
304
+ renderer.close()
305
+ finally:
306
+ raise
159
307
 
160
- async for event in local_agent.arun_a2a_stream(message, **kwargs):
161
- if verbose:
162
- self._log_event(event)
163
- events.append(event)
308
+ # Use shared finalizer to avoid code duplication
309
+ from glaip_sdk.client.run_rendering import finalize_render_manager # noqa: PLC0415
164
310
 
165
- return _event_processor.extract_final_response(events)
311
+ return finalize_render_manager(
312
+ render_manager, renderer, final_text, stats_usage, started_monotonic, finished_monotonic
313
+ )
166
314
 
167
315
  def build_langgraph_agent(
168
316
  self,
169
317
  agent: Agent,
170
318
  runtime_config: dict[str, Any] | None = None,
319
+ shared_tool_output_manager: Any | None = None,
320
+ *,
321
+ pause_resume_callback: Any | None = None,
171
322
  ) -> Any:
172
323
  """Build a LangGraphReactAgent from a glaip_sdk Agent definition.
173
324
 
@@ -175,6 +326,10 @@ class LangGraphRunner(BaseRunner):
175
326
  agent: The glaip_sdk Agent to convert.
176
327
  runtime_config: Optional runtime configuration with tool_configs,
177
328
  mcp_configs, agent_config, and agent-specific overrides.
329
+ shared_tool_output_manager: Optional ToolOutputManager to reuse across
330
+ agents with tool_output_sharing enabled.
331
+ pause_resume_callback: Optional callback used to pause/resume the renderer
332
+ during interactive HITL prompts.
178
333
 
179
334
  Returns:
180
335
  A configured LangGraphReactAgent instance.
@@ -184,17 +339,18 @@ class LangGraphRunner(BaseRunner):
184
339
  ValueError: If agent has unsupported tools, MCPs, or sub-agents for local mode.
185
340
  """
186
341
  from aip_agents.agent import LangGraphReactAgent # noqa: PLC0415
342
+
187
343
  from glaip_sdk.runner.tool_adapter import LangChainToolAdapter # noqa: PLC0415
188
344
 
189
345
  # Adapt tools for local execution
346
+ # NOTE: CLI parity waiver - local tool execution is SDK-only for MVP.
347
+ # See specs/f/local-agent-runtime/plan.md: "CLI parity is explicitly deferred
348
+ # and will require SDK Technical Lead sign-off per constitution principle IV."
190
349
  langchain_tools: list[Any] = []
191
350
  if agent.tools:
192
351
  adapter = LangChainToolAdapter()
193
352
  langchain_tools = adapter.adapt_tools(agent.tools)
194
353
 
195
- # Build sub-agents recursively
196
- sub_agent_instances = self._build_sub_agents(agent.agents, runtime_config)
197
-
198
354
  # Normalize runtime config: merge global and agent-specific configs
199
355
  normalized_config = self._normalize_runtime_config(runtime_config, agent)
200
356
 
@@ -208,6 +364,19 @@ class LangGraphRunner(BaseRunner):
208
364
  merged_agent_config = self._merge_agent_config(agent, normalized_config)
209
365
  agent_config_params, agent_config_kwargs = self._apply_agent_config(merged_agent_config)
210
366
 
367
+ tool_output_manager = self._resolve_tool_output_manager(
368
+ agent,
369
+ merged_agent_config,
370
+ shared_tool_output_manager,
371
+ )
372
+
373
+ # Build sub-agents recursively, sharing tool output manager when enabled.
374
+ sub_agent_instances = self._build_sub_agents(
375
+ agent.agents,
376
+ runtime_config,
377
+ shared_tool_output_manager=tool_output_manager,
378
+ )
379
+
211
380
  # Build the LangGraphReactAgent with tools, sub-agents, and configs
212
381
  local_agent = LangGraphReactAgent(
213
382
  name=agent.name,
@@ -217,6 +386,8 @@ class LangGraphRunner(BaseRunner):
217
386
  tools=langchain_tools,
218
387
  agents=sub_agent_instances if sub_agent_instances else None,
219
388
  tool_configs=tool_configs if tool_configs else None,
389
+ tool_output_manager=tool_output_manager,
390
+ guardrail=agent.guardrail,
220
391
  **agent_config_params,
221
392
  **agent_config_kwargs,
222
393
  )
@@ -224,6 +395,11 @@ class LangGraphRunner(BaseRunner):
224
395
  # Add MCP servers if configured
225
396
  self._add_mcp_servers(local_agent, agent, mcp_configs)
226
397
 
398
+ # Inject local HITL manager only if hitl_enabled is True (master switch).
399
+ # This matches remote behavior: hitl_enabled gates the HITL plumbing.
400
+ # Tool-level HITL configs are only enforced when hitl_enabled=True.
401
+ self._inject_hitl_manager(local_agent, merged_agent_config, agent.name, pause_resume_callback)
402
+
227
403
  logger.debug(
228
404
  "Built local LangGraphReactAgent for agent '%s' with %d tools, %d sub-agents, and %d MCPs",
229
405
  agent.name,
@@ -233,16 +409,63 @@ class LangGraphRunner(BaseRunner):
233
409
  )
234
410
  return local_agent
235
411
 
412
+ def _resolve_tool_output_manager(
413
+ self,
414
+ agent: Agent,
415
+ merged_agent_config: dict[str, Any],
416
+ shared_tool_output_manager: Any | None,
417
+ ) -> Any | None:
418
+ """Resolve tool output manager for local agent execution."""
419
+ tool_output_sharing_enabled = merged_agent_config.get("tool_output_sharing", False)
420
+ if not tool_output_sharing_enabled:
421
+ return None
422
+ if shared_tool_output_manager is not None:
423
+ return shared_tool_output_manager
424
+ return build_tool_output_manager(agent.name, merged_agent_config)
425
+
426
+ def _inject_hitl_manager(
427
+ self,
428
+ local_agent: Any,
429
+ merged_agent_config: dict[str, Any],
430
+ agent_name: str,
431
+ pause_resume_callback: Any | None,
432
+ ) -> None:
433
+ """Inject HITL manager when enabled, mirroring remote gating behavior."""
434
+ hitl_enabled = merged_agent_config.get("hitl_enabled", False)
435
+ if hitl_enabled:
436
+ try:
437
+ from aip_agents.agent.hitl.manager import ApprovalManager # noqa: PLC0415
438
+ from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
439
+
440
+ local_agent.hitl_manager = ApprovalManager(
441
+ prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
442
+ )
443
+ # Store callback reference for setting renderer later
444
+ if pause_resume_callback:
445
+ local_agent._pause_resume_callback = pause_resume_callback
446
+ logger.debug("HITL manager injected for agent '%s' (hitl_enabled=True)", agent_name)
447
+ except ImportError as e:
448
+ # Missing dependencies - fail fast
449
+ raise ImportError("Local HITL requires aip_agents. Install with: pip install 'glaip-sdk[local]'") from e
450
+ except Exception as e:
451
+ # Other errors during HITL setup - fail fast
452
+ raise RuntimeError(f"Failed to initialize HITL manager for agent '{agent_name}'") from e
453
+ else:
454
+ logger.debug("HITL manager not injected for agent '%s' (hitl_enabled=False)", agent_name)
455
+
236
456
  def _build_sub_agents(
237
457
  self,
238
458
  sub_agents: list[Any] | None,
239
459
  runtime_config: dict[str, Any] | None,
460
+ shared_tool_output_manager: Any | None = None,
240
461
  ) -> list[Any]:
241
462
  """Build sub-agent instances recursively.
242
463
 
243
464
  Args:
244
465
  sub_agents: List of sub-agent definitions.
245
466
  runtime_config: Runtime config to pass to sub-agents.
467
+ shared_tool_output_manager: Optional ToolOutputManager to reuse across
468
+ agents with tool_output_sharing enabled.
246
469
 
247
470
  Returns:
248
471
  List of built sub-agent instances.
@@ -255,15 +478,14 @@ class LangGraphRunner(BaseRunner):
255
478
 
256
479
  sub_agent_instances = []
257
480
  for sub_agent in sub_agents:
258
- if getattr(sub_agent, "_lookup_only", False):
259
- agent_name = getattr(sub_agent, "name", "<unknown>")
260
- raise ValueError(
261
- f"Sub-agent '{agent_name}' is not supported in local mode. "
262
- "Platform agents (from_id, from_native) cannot be used as "
263
- "sub-agents in local execution. "
264
- "Define the sub-agent locally with Agent(name=..., instruction=...) instead."
481
+ self._validate_sub_agent_for_local_mode(sub_agent)
482
+ sub_agent_instances.append(
483
+ self.build_langgraph_agent(
484
+ sub_agent,
485
+ runtime_config,
486
+ shared_tool_output_manager=shared_tool_output_manager,
265
487
  )
266
- sub_agent_instances.append(self.build_langgraph_agent(sub_agent, runtime_config))
488
+ )
267
489
  return sub_agent_instances
268
490
 
269
491
  def _add_mcp_servers(
@@ -286,16 +508,12 @@ class LangGraphRunner(BaseRunner):
286
508
 
287
509
  mcp_adapter = LangChainMCPAdapter()
288
510
  base_mcp_configs = mcp_adapter.adapt_mcps(agent.mcps)
289
- logger.debug("Base MCP configs from adapter: %s", base_mcp_configs)
290
511
 
291
512
  # Apply merged mcp_configs overrides (agent definition + runtime)
292
- logger.debug("Merged mcp_configs to apply: %s", merged_mcp_configs)
293
513
  if merged_mcp_configs:
294
514
  base_mcp_configs = self._apply_runtime_mcp_configs(base_mcp_configs, merged_mcp_configs)
295
- logger.debug("MCP configs after override: %s", base_mcp_configs)
296
515
 
297
516
  if base_mcp_configs:
298
- logger.info("MCP configs being sent to aip-agents: %s", base_mcp_configs)
299
517
  local_agent.add_mcp_server(base_mcp_configs)
300
518
  logger.debug(
301
519
  "Registered %d MCP server(s) for agent '%s'",
@@ -361,6 +579,7 @@ class LangGraphRunner(BaseRunner):
361
579
  Returns:
362
580
  Agent-specific config dict, or empty dict if not found.
363
581
  """
582
+ from glaip_sdk.utils.resource_refs import is_uuid # noqa: PLC0415
364
583
  from glaip_sdk.utils.runtime_config import get_name_from_key # noqa: PLC0415
365
584
 
366
585
  # Reserved keys at the top level
@@ -371,6 +590,14 @@ class LangGraphRunner(BaseRunner):
371
590
  if key in reserved_keys:
372
591
  continue # Skip global configs
373
592
 
593
+ if isinstance(key, str) and is_uuid(key):
594
+ logger.warning(
595
+ "UUID agent override key '%s' is not supported in local mode; skipping. "
596
+ "Use agent name string or Agent instance as the key instead.",
597
+ key,
598
+ )
599
+ continue
600
+
374
601
  # Check if this key matches the agent
375
602
  try:
376
603
  key_name = get_name_from_key(key)
@@ -505,7 +732,13 @@ class LangGraphRunner(BaseRunner):
505
732
  if "planning" in agent_config:
506
733
  direct_params["planning"] = agent_config["planning"]
507
734
 
735
+ if "enable_a2a_token_streaming" in agent_config:
736
+ direct_params["enable_a2a_token_streaming"] = agent_config["enable_a2a_token_streaming"]
737
+
508
738
  # Kwargs parameters (passed through **kwargs to BaseAgent)
739
+ if "enable_pii" in agent_config:
740
+ kwargs_params["enable_pii"] = agent_config["enable_pii"]
741
+
509
742
  if "memory" in agent_config:
510
743
  # Map "memory" to "memory_backend" for aip-agents compatibility
511
744
  kwargs_params["memory_backend"] = agent_config["memory"]
@@ -579,6 +812,49 @@ class LangGraphRunner(BaseRunner):
579
812
 
580
813
  return merged
581
814
 
815
+ def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
816
+ """Validate that a sub-agent reference is supported for local execution.
817
+
818
+ Args:
819
+ sub_agent: The sub-agent reference to validate.
820
+
821
+ Raises:
822
+ ValueError: If the sub-agent is not supported in local mode.
823
+ """
824
+ # String references are allowed by SDK API but not for local mode
825
+ if isinstance(sub_agent, str):
826
+ raise ValueError(
827
+ f"Sub-agent '{sub_agent}' is a string reference and cannot be used in local mode. "
828
+ "String sub-agent references are only supported for server execution. "
829
+ "For local mode, define the sub-agent with Agent(name=..., instruction=...)."
830
+ )
831
+
832
+ # Validate sub-agent is not a class
833
+ if inspect.isclass(sub_agent):
834
+ raise ValueError(
835
+ f"Sub-agent '{sub_agent.__name__}' is a class, not an instance. "
836
+ "Local mode requires Agent INSTANCES. "
837
+ "Did you forget to instantiate it? e.g., Agent(...), not Agent"
838
+ )
839
+
840
+ # Validate sub-agent is an Agent-like object (has required attributes)
841
+ if not hasattr(sub_agent, "name") or not hasattr(sub_agent, "instruction"):
842
+ raise ValueError(
843
+ f"Sub-agent {type(sub_agent).__name__} is not supported in local mode. "
844
+ "Local mode requires Agent instances with 'name' and 'instruction' attributes. "
845
+ "Define the sub-agent with Agent(name=..., instruction=...)."
846
+ )
847
+
848
+ # Validate sub-agent is not platform-only (from_id, from_native)
849
+ if getattr(sub_agent, "_lookup_only", False):
850
+ agent_name = getattr(sub_agent, "name", "<unknown>")
851
+ raise ValueError(
852
+ f"Sub-agent '{agent_name}' is not supported in local mode. "
853
+ "Platform agents (from_id, from_native) cannot be used as "
854
+ "sub-agents in local execution. "
855
+ "Define the sub-agent locally with Agent(name=..., instruction=...) instead."
856
+ )
857
+
582
858
  def _log_event(self, event: dict[str, Any]) -> None:
583
859
  """Log an A2AEvent for verbose debug output.
584
860