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
@@ -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,7 @@ 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,
220
390
  **agent_config_params,
221
391
  **agent_config_kwargs,
222
392
  )
@@ -224,6 +394,11 @@ class LangGraphRunner(BaseRunner):
224
394
  # Add MCP servers if configured
225
395
  self._add_mcp_servers(local_agent, agent, mcp_configs)
226
396
 
397
+ # Inject local HITL manager only if hitl_enabled is True (master switch).
398
+ # This matches remote behavior: hitl_enabled gates the HITL plumbing.
399
+ # Tool-level HITL configs are only enforced when hitl_enabled=True.
400
+ self._inject_hitl_manager(local_agent, merged_agent_config, agent.name, pause_resume_callback)
401
+
227
402
  logger.debug(
228
403
  "Built local LangGraphReactAgent for agent '%s' with %d tools, %d sub-agents, and %d MCPs",
229
404
  agent.name,
@@ -233,16 +408,63 @@ class LangGraphRunner(BaseRunner):
233
408
  )
234
409
  return local_agent
235
410
 
411
+ def _resolve_tool_output_manager(
412
+ self,
413
+ agent: Agent,
414
+ merged_agent_config: dict[str, Any],
415
+ shared_tool_output_manager: Any | None,
416
+ ) -> Any | None:
417
+ """Resolve tool output manager for local agent execution."""
418
+ tool_output_sharing_enabled = merged_agent_config.get("tool_output_sharing", False)
419
+ if not tool_output_sharing_enabled:
420
+ return None
421
+ if shared_tool_output_manager is not None:
422
+ return shared_tool_output_manager
423
+ return build_tool_output_manager(agent.name, merged_agent_config)
424
+
425
+ def _inject_hitl_manager(
426
+ self,
427
+ local_agent: Any,
428
+ merged_agent_config: dict[str, Any],
429
+ agent_name: str,
430
+ pause_resume_callback: Any | None,
431
+ ) -> None:
432
+ """Inject HITL manager when enabled, mirroring remote gating behavior."""
433
+ hitl_enabled = merged_agent_config.get("hitl_enabled", False)
434
+ if hitl_enabled:
435
+ try:
436
+ from aip_agents.agent.hitl.manager import ApprovalManager # noqa: PLC0415
437
+ from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
438
+
439
+ local_agent.hitl_manager = ApprovalManager(
440
+ prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
441
+ )
442
+ # Store callback reference for setting renderer later
443
+ if pause_resume_callback:
444
+ local_agent._pause_resume_callback = pause_resume_callback
445
+ logger.debug("HITL manager injected for agent '%s' (hitl_enabled=True)", agent_name)
446
+ except ImportError as e:
447
+ # Missing dependencies - fail fast
448
+ raise ImportError("Local HITL requires aip_agents. Install with: pip install 'glaip-sdk[local]'") from e
449
+ except Exception as e:
450
+ # Other errors during HITL setup - fail fast
451
+ raise RuntimeError(f"Failed to initialize HITL manager for agent '{agent_name}'") from e
452
+ else:
453
+ logger.debug("HITL manager not injected for agent '%s' (hitl_enabled=False)", agent_name)
454
+
236
455
  def _build_sub_agents(
237
456
  self,
238
457
  sub_agents: list[Any] | None,
239
458
  runtime_config: dict[str, Any] | None,
459
+ shared_tool_output_manager: Any | None = None,
240
460
  ) -> list[Any]:
241
461
  """Build sub-agent instances recursively.
242
462
 
243
463
  Args:
244
464
  sub_agents: List of sub-agent definitions.
245
465
  runtime_config: Runtime config to pass to sub-agents.
466
+ shared_tool_output_manager: Optional ToolOutputManager to reuse across
467
+ agents with tool_output_sharing enabled.
246
468
 
247
469
  Returns:
248
470
  List of built sub-agent instances.
@@ -255,15 +477,14 @@ class LangGraphRunner(BaseRunner):
255
477
 
256
478
  sub_agent_instances = []
257
479
  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."
480
+ self._validate_sub_agent_for_local_mode(sub_agent)
481
+ sub_agent_instances.append(
482
+ self.build_langgraph_agent(
483
+ sub_agent,
484
+ runtime_config,
485
+ shared_tool_output_manager=shared_tool_output_manager,
265
486
  )
266
- sub_agent_instances.append(self.build_langgraph_agent(sub_agent, runtime_config))
487
+ )
267
488
  return sub_agent_instances
268
489
 
269
490
  def _add_mcp_servers(
@@ -286,16 +507,12 @@ class LangGraphRunner(BaseRunner):
286
507
 
287
508
  mcp_adapter = LangChainMCPAdapter()
288
509
  base_mcp_configs = mcp_adapter.adapt_mcps(agent.mcps)
289
- logger.debug("Base MCP configs from adapter: %s", base_mcp_configs)
290
510
 
291
511
  # Apply merged mcp_configs overrides (agent definition + runtime)
292
- logger.debug("Merged mcp_configs to apply: %s", merged_mcp_configs)
293
512
  if merged_mcp_configs:
294
513
  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
514
 
297
515
  if base_mcp_configs:
298
- logger.info("MCP configs being sent to aip-agents: %s", base_mcp_configs)
299
516
  local_agent.add_mcp_server(base_mcp_configs)
300
517
  logger.debug(
301
518
  "Registered %d MCP server(s) for agent '%s'",
@@ -361,6 +578,7 @@ class LangGraphRunner(BaseRunner):
361
578
  Returns:
362
579
  Agent-specific config dict, or empty dict if not found.
363
580
  """
581
+ from glaip_sdk.utils.resource_refs import is_uuid # noqa: PLC0415
364
582
  from glaip_sdk.utils.runtime_config import get_name_from_key # noqa: PLC0415
365
583
 
366
584
  # Reserved keys at the top level
@@ -371,6 +589,14 @@ class LangGraphRunner(BaseRunner):
371
589
  if key in reserved_keys:
372
590
  continue # Skip global configs
373
591
 
592
+ if isinstance(key, str) and is_uuid(key):
593
+ logger.warning(
594
+ "UUID agent override key '%s' is not supported in local mode; skipping. "
595
+ "Use agent name string or Agent instance as the key instead.",
596
+ key,
597
+ )
598
+ continue
599
+
374
600
  # Check if this key matches the agent
375
601
  try:
376
602
  key_name = get_name_from_key(key)
@@ -505,7 +731,13 @@ class LangGraphRunner(BaseRunner):
505
731
  if "planning" in agent_config:
506
732
  direct_params["planning"] = agent_config["planning"]
507
733
 
734
+ if "enable_a2a_token_streaming" in agent_config:
735
+ direct_params["enable_a2a_token_streaming"] = agent_config["enable_a2a_token_streaming"]
736
+
508
737
  # Kwargs parameters (passed through **kwargs to BaseAgent)
738
+ if "enable_pii" in agent_config:
739
+ kwargs_params["enable_pii"] = agent_config["enable_pii"]
740
+
509
741
  if "memory" in agent_config:
510
742
  # Map "memory" to "memory_backend" for aip-agents compatibility
511
743
  kwargs_params["memory_backend"] = agent_config["memory"]
@@ -579,6 +811,49 @@ class LangGraphRunner(BaseRunner):
579
811
 
580
812
  return merged
581
813
 
814
+ def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
815
+ """Validate that a sub-agent reference is supported for local execution.
816
+
817
+ Args:
818
+ sub_agent: The sub-agent reference to validate.
819
+
820
+ Raises:
821
+ ValueError: If the sub-agent is not supported in local mode.
822
+ """
823
+ # String references are allowed by SDK API but not for local mode
824
+ if isinstance(sub_agent, str):
825
+ raise ValueError(
826
+ f"Sub-agent '{sub_agent}' is a string reference and cannot be used in local mode. "
827
+ "String sub-agent references are only supported for server execution. "
828
+ "For local mode, define the sub-agent with Agent(name=..., instruction=...)."
829
+ )
830
+
831
+ # Validate sub-agent is not a class
832
+ if inspect.isclass(sub_agent):
833
+ raise ValueError(
834
+ f"Sub-agent '{sub_agent.__name__}' is a class, not an instance. "
835
+ "Local mode requires Agent INSTANCES. "
836
+ "Did you forget to instantiate it? e.g., Agent(...), not Agent"
837
+ )
838
+
839
+ # Validate sub-agent is an Agent-like object (has required attributes)
840
+ if not hasattr(sub_agent, "name") or not hasattr(sub_agent, "instruction"):
841
+ raise ValueError(
842
+ f"Sub-agent {type(sub_agent).__name__} is not supported in local mode. "
843
+ "Local mode requires Agent instances with 'name' and 'instruction' attributes. "
844
+ "Define the sub-agent with Agent(name=..., instruction=...)."
845
+ )
846
+
847
+ # Validate sub-agent is not platform-only (from_id, from_native)
848
+ if getattr(sub_agent, "_lookup_only", False):
849
+ agent_name = getattr(sub_agent, "name", "<unknown>")
850
+ raise ValueError(
851
+ f"Sub-agent '{agent_name}' is not supported in local mode. "
852
+ "Platform agents (from_id, from_native) cannot be used as "
853
+ "sub-agents in local execution. "
854
+ "Define the sub-agent locally with Agent(name=..., instruction=...) instead."
855
+ )
856
+
582
857
  def _log_event(self, event: dict[str, Any]) -> None:
583
858
  """Log an A2AEvent for verbose debug output.
584
859