glaip-sdk 0.6.10__py3-none-any.whl → 0.7.27__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 (139) hide show
  1. glaip_sdk/__init__.py +42 -5
  2. glaip_sdk/agents/base.py +295 -37
  3. glaip_sdk/agents/component.py +233 -0
  4. glaip_sdk/branding.py +113 -2
  5. glaip_sdk/cli/account_store.py +15 -0
  6. glaip_sdk/cli/auth.py +14 -8
  7. glaip_sdk/cli/commands/accounts.py +1 -1
  8. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  9. glaip_sdk/cli/commands/agents/_common.py +562 -0
  10. glaip_sdk/cli/commands/agents/create.py +155 -0
  11. glaip_sdk/cli/commands/agents/delete.py +64 -0
  12. glaip_sdk/cli/commands/agents/get.py +89 -0
  13. glaip_sdk/cli/commands/agents/list.py +129 -0
  14. glaip_sdk/cli/commands/agents/run.py +264 -0
  15. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  16. glaip_sdk/cli/commands/agents/update.py +112 -0
  17. glaip_sdk/cli/commands/common_config.py +15 -12
  18. glaip_sdk/cli/commands/configure.py +1 -2
  19. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  20. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  21. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  22. glaip_sdk/cli/commands/mcps/create.py +152 -0
  23. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  24. glaip_sdk/cli/commands/mcps/get.py +212 -0
  25. glaip_sdk/cli/commands/mcps/list.py +69 -0
  26. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  27. glaip_sdk/cli/commands/mcps/update.py +190 -0
  28. glaip_sdk/cli/commands/models.py +2 -4
  29. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  30. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  31. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  32. glaip_sdk/cli/commands/tools/_common.py +80 -0
  33. glaip_sdk/cli/commands/tools/create.py +228 -0
  34. glaip_sdk/cli/commands/tools/delete.py +61 -0
  35. glaip_sdk/cli/commands/tools/get.py +103 -0
  36. glaip_sdk/cli/commands/tools/list.py +69 -0
  37. glaip_sdk/cli/commands/tools/script.py +49 -0
  38. glaip_sdk/cli/commands/tools/update.py +102 -0
  39. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  40. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  41. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  42. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  43. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  44. glaip_sdk/cli/commands/update.py +163 -17
  45. glaip_sdk/cli/config.py +1 -0
  46. glaip_sdk/cli/core/output.py +12 -7
  47. glaip_sdk/cli/entrypoint.py +20 -0
  48. glaip_sdk/cli/main.py +127 -39
  49. glaip_sdk/cli/pager.py +3 -3
  50. glaip_sdk/cli/resolution.py +2 -1
  51. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  52. glaip_sdk/cli/slash/agent_session.py +1 -1
  53. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  54. glaip_sdk/cli/slash/session.py +343 -20
  55. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  56. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  57. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  58. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  59. glaip_sdk/cli/slash/tui/context.py +92 -0
  60. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  61. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  62. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  63. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  64. glaip_sdk/cli/slash/tui/loading.py +43 -21
  65. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  66. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  67. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  68. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  69. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  70. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  71. glaip_sdk/cli/slash/tui/toast.py +388 -0
  72. glaip_sdk/cli/transcript/history.py +1 -1
  73. glaip_sdk/cli/transcript/viewer.py +1 -1
  74. glaip_sdk/cli/tui_settings.py +125 -0
  75. glaip_sdk/cli/update_notifier.py +215 -7
  76. glaip_sdk/cli/validators.py +1 -1
  77. glaip_sdk/client/__init__.py +2 -1
  78. glaip_sdk/client/_schedule_payloads.py +89 -0
  79. glaip_sdk/client/agents.py +290 -16
  80. glaip_sdk/client/base.py +25 -0
  81. glaip_sdk/client/hitl.py +136 -0
  82. glaip_sdk/client/main.py +7 -5
  83. glaip_sdk/client/mcps.py +44 -13
  84. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  85. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  86. glaip_sdk/client/payloads/agent/responses.py +43 -0
  87. glaip_sdk/client/run_rendering.py +414 -3
  88. glaip_sdk/client/schedules.py +439 -0
  89. glaip_sdk/client/tools.py +52 -23
  90. glaip_sdk/config/constants.py +22 -2
  91. glaip_sdk/guardrails/__init__.py +80 -0
  92. glaip_sdk/guardrails/serializer.py +91 -0
  93. glaip_sdk/hitl/__init__.py +48 -0
  94. glaip_sdk/hitl/base.py +64 -0
  95. glaip_sdk/hitl/callback.py +43 -0
  96. glaip_sdk/hitl/local.py +121 -0
  97. glaip_sdk/hitl/remote.py +523 -0
  98. glaip_sdk/models/__init__.py +47 -1
  99. glaip_sdk/models/_provider_mappings.py +101 -0
  100. glaip_sdk/models/_validation.py +97 -0
  101. glaip_sdk/models/agent.py +2 -1
  102. glaip_sdk/models/agent_runs.py +2 -1
  103. glaip_sdk/models/constants.py +141 -0
  104. glaip_sdk/models/model.py +170 -0
  105. glaip_sdk/models/schedule.py +224 -0
  106. glaip_sdk/payload_schemas/agent.py +1 -0
  107. glaip_sdk/payload_schemas/guardrails.py +34 -0
  108. glaip_sdk/ptc.py +145 -0
  109. glaip_sdk/registry/tool.py +270 -57
  110. glaip_sdk/runner/__init__.py +20 -3
  111. glaip_sdk/runner/deps.py +6 -6
  112. glaip_sdk/runner/langgraph.py +427 -39
  113. glaip_sdk/runner/logging_config.py +77 -0
  114. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  115. glaip_sdk/runner/ptc_adapter.py +98 -0
  116. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  117. glaip_sdk/schedules/__init__.py +22 -0
  118. glaip_sdk/schedules/base.py +291 -0
  119. glaip_sdk/tools/base.py +67 -14
  120. glaip_sdk/utils/__init__.py +1 -0
  121. glaip_sdk/utils/agent_config.py +8 -2
  122. glaip_sdk/utils/bundler.py +138 -2
  123. glaip_sdk/utils/import_resolver.py +427 -49
  124. glaip_sdk/utils/rendering/renderer/base.py +58 -0
  125. glaip_sdk/utils/runtime_config.py +3 -2
  126. glaip_sdk/utils/sync.py +31 -11
  127. glaip_sdk/utils/tool_detection.py +274 -6
  128. glaip_sdk/utils/tool_storage_provider.py +140 -0
  129. {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +51 -40
  130. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  131. {glaip_sdk-0.6.10.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +2 -1
  132. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  133. glaip_sdk-0.7.27.dist-info/top_level.txt +1 -0
  134. glaip_sdk/cli/commands/agents.py +0 -1509
  135. glaip_sdk/cli/commands/mcps.py +0 -1356
  136. glaip_sdk/cli/commands/tools.py +0 -576
  137. glaip_sdk/cli/utils.py +0 -263
  138. glaip_sdk-0.6.10.dist-info/RECORD +0 -159
  139. glaip_sdk-0.6.10.dist-info/entry_points.txt +0 -3
@@ -1,3 +1,4 @@
1
+ # pylint: disable=duplicate-code
1
2
  """LangGraph-based runner for local agent execution.
2
3
 
3
4
  This module provides the LangGraphRunner which executes glaip-sdk agents
@@ -19,26 +20,64 @@ from __future__ import annotations
19
20
 
20
21
  import asyncio
21
22
  import inspect
23
+ import logging
22
24
  from dataclasses import dataclass
23
25
  from typing import TYPE_CHECKING, Any
24
26
 
27
+ from gllm_core.utils import LoggerManager
28
+
29
+ from glaip_sdk.client.run_rendering import AgentRunRenderingManager
30
+ from glaip_sdk.hitl import PauseResumeCallback
31
+ from glaip_sdk.models import DEFAULT_MODEL
25
32
  from glaip_sdk.runner.base import BaseRunner
26
33
  from glaip_sdk.runner.deps import (
27
34
  check_local_runtime_available,
28
35
  get_local_runtime_missing_message,
29
36
  )
30
- from glaip_sdk.utils.a2a import A2AEventStreamProcessor
31
- from gllm_core.utils import LoggerManager
37
+ from glaip_sdk.runner.ptc_adapter import (
38
+ normalize_ptc_for_aip_agents,
39
+ validate_ptc_for_local_run,
40
+ )
41
+ from glaip_sdk.utils.tool_storage_provider import build_tool_output_manager
32
42
 
33
43
  if TYPE_CHECKING:
34
44
  from langchain_core.messages import BaseMessage
35
45
 
36
46
  from glaip_sdk.agents.base import Agent
37
47
 
48
+
49
+ _AIP_LOGS_SWALLOWED = False
50
+
51
+
52
+ def _swallow_aip_logs(level: int = logging.ERROR) -> None:
53
+ """Consume noisy AIPAgents logs once (opt-in via runner flag)."""
54
+ global _AIP_LOGS_SWALLOWED
55
+ if _AIP_LOGS_SWALLOWED:
56
+ return
57
+ prefixes = ("aip_agents.",)
58
+
59
+ def _silence(name: str) -> None:
60
+ lg = logging.getLogger(name)
61
+ lg.handlers = [logging.NullHandler()]
62
+ lg.propagate = False
63
+ lg.setLevel(level)
64
+
65
+ # Silence any already-registered loggers under the given prefixes
66
+ for logger_name in logging.root.manager.loggerDict:
67
+ if any(logger_name.startswith(prefix) for prefix in prefixes):
68
+ _silence(logger_name)
69
+
70
+ # Also set the base prefix loggers so future children inherit silence
71
+ for prefix in prefixes:
72
+ _silence(prefix.rstrip("."))
73
+ _AIP_LOGS_SWALLOWED = True
74
+
75
+
38
76
  logger = LoggerManager().get_logger(__name__)
39
77
 
40
- # Default A2A event processor
41
- _event_processor = A2AEventStreamProcessor()
78
+
79
+ # Constants for MCP configuration validation
80
+ _MCP_TRANSPORT_KEYS = {"url", "command", "args", "env", "timeout", "headers"}
42
81
 
43
82
 
44
83
  def _convert_chat_history_to_messages(
@@ -56,7 +95,11 @@ def _convert_chat_history_to_messages(
56
95
  if not chat_history:
57
96
  return []
58
97
 
59
- from langchain_core.messages import AIMessage, HumanMessage, SystemMessage # noqa: PLC0415
98
+ from langchain_core.messages import ( # noqa: PLC0415
99
+ AIMessage,
100
+ HumanMessage,
101
+ SystemMessage,
102
+ )
60
103
 
61
104
  messages: list[BaseMessage] = []
62
105
  for msg in chat_history:
@@ -81,16 +124,17 @@ def _convert_chat_history_to_messages(
81
124
  class LangGraphRunner(BaseRunner):
82
125
  """Runner implementation using aip-agents LangGraphReactAgent.
83
126
 
84
- MVP scope:
85
- - Execute via `LangGraphReactAgent.arun_a2a_stream()`
86
- - Extract and return final text from the emitted `final_response` event
127
+ Current behavior:
128
+ - Execute via `LangGraphReactAgent.arun_sse_stream()` (normalized SSE-compatible stream)
129
+ - Route all events through `AgentRunRenderingManager.async_process_stream_events`
130
+ for unified rendering between local and remote agents
87
131
 
88
132
  Attributes:
89
133
  default_model: Model name to use when agent.model is not set.
90
134
  Defaults to "gpt-4o-mini".
91
135
  """
92
136
 
93
- default_model: str = "openai/gpt-4o-mini"
137
+ default_model: str = DEFAULT_MODEL
94
138
 
95
139
  def run(
96
140
  self,
@@ -99,6 +143,8 @@ class LangGraphRunner(BaseRunner):
99
143
  verbose: bool = False,
100
144
  runtime_config: dict[str, Any] | None = None,
101
145
  chat_history: list[dict[str, str]] | None = None,
146
+ *,
147
+ swallow_aip_logs: bool = True,
102
148
  **kwargs: Any,
103
149
  ) -> str:
104
150
  """Execute agent synchronously and return final response text.
@@ -113,6 +159,9 @@ class LangGraphRunner(BaseRunner):
113
159
  chat_history: Optional list of prior conversation messages.
114
160
  Each message is a dict with "role" and "content" keys.
115
161
  Defaults to None.
162
+ swallow_aip_logs: When True (default), silence noisy logs from aip-agents,
163
+ gllm_inference, OpenAILMInvoker, and httpx. Set to False to honor user
164
+ logging configuration.
116
165
  **kwargs: Additional keyword arguments passed to the backend.
117
166
 
118
167
  Returns:
@@ -141,6 +190,7 @@ class LangGraphRunner(BaseRunner):
141
190
  verbose=verbose,
142
191
  runtime_config=runtime_config,
143
192
  chat_history=chat_history,
193
+ swallow_aip_logs=swallow_aip_logs,
144
194
  **kwargs,
145
195
  )
146
196
 
@@ -153,6 +203,8 @@ class LangGraphRunner(BaseRunner):
153
203
  verbose: bool = False,
154
204
  runtime_config: dict[str, Any] | None = None,
155
205
  chat_history: list[dict[str, str]] | None = None,
206
+ *,
207
+ swallow_aip_logs: bool = True,
156
208
  **kwargs: Any,
157
209
  ) -> str:
158
210
  """Execute agent asynchronously and return final response text.
@@ -167,6 +219,7 @@ class LangGraphRunner(BaseRunner):
167
219
  chat_history: Optional list of prior conversation messages.
168
220
  Each message is a dict with "role" and "content" keys.
169
221
  Defaults to None.
222
+ swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
170
223
  **kwargs: Additional keyword arguments passed to the backend.
171
224
 
172
225
  Returns:
@@ -181,6 +234,7 @@ class LangGraphRunner(BaseRunner):
181
234
  verbose=verbose,
182
235
  runtime_config=runtime_config,
183
236
  chat_history=chat_history,
237
+ swallow_aip_logs=swallow_aip_logs,
184
238
  **kwargs,
185
239
  )
186
240
 
@@ -191,6 +245,8 @@ class LangGraphRunner(BaseRunner):
191
245
  verbose: bool = False,
192
246
  runtime_config: dict[str, Any] | None = None,
193
247
  chat_history: list[dict[str, str]] | None = None,
248
+ *,
249
+ swallow_aip_logs: bool = True,
194
250
  **kwargs: Any,
195
251
  ) -> str:
196
252
  """Internal async implementation of agent execution.
@@ -201,13 +257,25 @@ class LangGraphRunner(BaseRunner):
201
257
  verbose: If True, emit debug trace output during execution.
202
258
  runtime_config: Optional runtime configuration for tools, MCPs, etc.
203
259
  chat_history: Optional list of prior conversation messages.
260
+ swallow_aip_logs: When True (default), silence noisy AIPAgents logs.
204
261
  **kwargs: Additional keyword arguments passed to the backend.
205
262
 
206
263
  Returns:
207
264
  The final response text from the agent.
208
265
  """
266
+ # Optionally swallow noisy AIPAgents logs
267
+ if swallow_aip_logs:
268
+ _swallow_aip_logs()
269
+
270
+ # POC/MVP: Create pause/resume callback for interactive HITL input
271
+ pause_resume_callback = PauseResumeCallback()
272
+
209
273
  # Build the local LangGraphReactAgent from the glaip_sdk Agent
210
- local_agent = self.build_langgraph_agent(agent, runtime_config=runtime_config)
274
+ local_agent = self.build_langgraph_agent(
275
+ agent,
276
+ runtime_config=runtime_config,
277
+ pause_resume_callback=pause_resume_callback,
278
+ )
211
279
 
212
280
  # Convert chat history to LangChain messages for the agent
213
281
  langchain_messages = _convert_chat_history_to_messages(chat_history)
@@ -219,20 +287,68 @@ class LangGraphRunner(BaseRunner):
219
287
  agent.name,
220
288
  )
221
289
 
222
- # Collect A2AEvents from the stream and extract final response
223
- events: list[dict[str, Any]] = []
290
+ # Use shared render manager for unified processing
291
+ render_manager = AgentRunRenderingManager(logger)
292
+ renderer = render_manager.create_renderer(kwargs.get("renderer"), verbose=verbose)
293
+
294
+ # POC/MVP: Set renderer on callback so LocalPromptHandler can pause/resume Live
295
+ pause_resume_callback.set_renderer(renderer)
296
+
297
+ meta = render_manager.build_initial_metadata(agent.name, message, kwargs)
298
+ render_manager.start_renderer(renderer, meta)
299
+
300
+ try:
301
+ # Use shared async stream processor for unified event handling
302
+ (
303
+ final_text,
304
+ stats_usage,
305
+ started_monotonic,
306
+ finished_monotonic,
307
+ ) = await render_manager.async_process_stream_events(
308
+ local_agent.arun_sse_stream(message, **kwargs),
309
+ renderer,
310
+ meta,
311
+ skip_final_render=True,
312
+ )
313
+ except KeyboardInterrupt:
314
+ try:
315
+ renderer.close()
316
+ finally:
317
+ raise
318
+ except Exception:
319
+ try:
320
+ renderer.close()
321
+ finally:
322
+ raise
323
+ finally:
324
+ # Cleanup PTC sandbox and MCP sessions
325
+ # Isolated cleanup steps so one failure doesn't skip the other
326
+ try:
327
+ await local_agent.cleanup()
328
+ except Exception as e:
329
+ logger.warning("Failed to cleanup agent resources: %s", e)
224
330
 
225
- async for event in local_agent.arun_a2a_stream(message, **kwargs):
226
- if verbose:
227
- self._log_event(event)
228
- events.append(event)
331
+ # Use shared finalizer to avoid code duplication
332
+ from glaip_sdk.client.run_rendering import ( # noqa: PLC0415
333
+ finalize_render_manager,
334
+ )
229
335
 
230
- return _event_processor.extract_final_response(events)
336
+ return finalize_render_manager(
337
+ render_manager,
338
+ renderer,
339
+ final_text,
340
+ stats_usage,
341
+ started_monotonic,
342
+ finished_monotonic,
343
+ )
231
344
 
232
345
  def build_langgraph_agent(
233
346
  self,
234
347
  agent: Agent,
235
348
  runtime_config: dict[str, Any] | None = None,
349
+ shared_tool_output_manager: Any | None = None,
350
+ *,
351
+ pause_resume_callback: Any | None = None,
236
352
  ) -> Any:
237
353
  """Build a LangGraphReactAgent from a glaip_sdk Agent definition.
238
354
 
@@ -240,6 +356,10 @@ class LangGraphRunner(BaseRunner):
240
356
  agent: The glaip_sdk Agent to convert.
241
357
  runtime_config: Optional runtime configuration with tool_configs,
242
358
  mcp_configs, agent_config, and agent-specific overrides.
359
+ shared_tool_output_manager: Optional ToolOutputManager to reuse across
360
+ agents with tool_output_sharing enabled.
361
+ pause_resume_callback: Optional callback used to pause/resume the renderer
362
+ during interactive HITL prompts.
243
363
 
244
364
  Returns:
245
365
  A configured LangGraphReactAgent instance.
@@ -249,6 +369,7 @@ class LangGraphRunner(BaseRunner):
249
369
  ValueError: If agent has unsupported tools, MCPs, or sub-agents for local mode.
250
370
  """
251
371
  from aip_agents.agent import LangGraphReactAgent # noqa: PLC0415
372
+
252
373
  from glaip_sdk.runner.tool_adapter import LangChainToolAdapter # noqa: PLC0415
253
374
 
254
375
  # Adapt tools for local execution
@@ -260,9 +381,6 @@ class LangGraphRunner(BaseRunner):
260
381
  adapter = LangChainToolAdapter()
261
382
  langchain_tools = adapter.adapt_tools(agent.tools)
262
383
 
263
- # Build sub-agents recursively
264
- sub_agent_instances = self._build_sub_agents(agent.agents, runtime_config)
265
-
266
384
  # Normalize runtime config: merge global and agent-specific configs
267
385
  normalized_config = self._normalize_runtime_config(runtime_config, agent)
268
386
 
@@ -276,15 +394,42 @@ class LangGraphRunner(BaseRunner):
276
394
  merged_agent_config = self._merge_agent_config(agent, normalized_config)
277
395
  agent_config_params, agent_config_kwargs = self._apply_agent_config(merged_agent_config)
278
396
 
279
- # Build the LangGraphReactAgent with tools, sub-agents, and configs
397
+ # Validate and normalize PTC configuration for local runs
398
+ ptc_config = validate_ptc_for_local_run(
399
+ agent_ptc=agent.ptc if hasattr(agent, "ptc") else None,
400
+ agent_config_ptc=None, # Already validated in _merge_agent_config
401
+ runtime_config_ptc=None, # Already validated in _normalize_runtime_config
402
+ )
403
+ normalized_ptc = normalize_ptc_for_aip_agents(ptc_config)
404
+
405
+ # Resolve model and merge its configuration into agent kwargs
406
+ model_string = self._resolve_local_model(agent, agent_config_kwargs)
407
+
408
+ tool_output_manager = self._resolve_tool_output_manager(
409
+ agent,
410
+ merged_agent_config,
411
+ shared_tool_output_manager,
412
+ )
413
+
414
+ # Build sub-agents recursively, sharing tool output manager when enabled.
415
+ sub_agent_instances = self._build_sub_agents(
416
+ agent.agents,
417
+ runtime_config,
418
+ shared_tool_output_manager=tool_output_manager,
419
+ )
420
+
421
+ # Build the LangGraphReactAgent with tools, sub-agents, configs, and PTC
280
422
  local_agent = LangGraphReactAgent(
281
423
  name=agent.name,
282
424
  instruction=agent.instruction,
283
425
  description=agent.description,
284
- model=agent.model or self.default_model,
426
+ model=model_string,
285
427
  tools=langchain_tools,
286
428
  agents=sub_agent_instances if sub_agent_instances else None,
287
429
  tool_configs=tool_configs if tool_configs else None,
430
+ tool_output_manager=tool_output_manager,
431
+ guardrail=agent.guardrail,
432
+ ptc_config=normalized_ptc,
288
433
  **agent_config_params,
289
434
  **agent_config_kwargs,
290
435
  )
@@ -292,6 +437,11 @@ class LangGraphRunner(BaseRunner):
292
437
  # Add MCP servers if configured
293
438
  self._add_mcp_servers(local_agent, agent, mcp_configs)
294
439
 
440
+ # Inject local HITL manager only if hitl_enabled is True (master switch).
441
+ # This matches remote behavior: hitl_enabled gates the HITL plumbing.
442
+ # Tool-level HITL configs are only enforced when hitl_enabled=True.
443
+ self._inject_hitl_manager(local_agent, merged_agent_config, agent.name, pause_resume_callback)
444
+
295
445
  logger.debug(
296
446
  "Built local LangGraphReactAgent for agent '%s' with %d tools, %d sub-agents, and %d MCPs",
297
447
  agent.name,
@@ -301,16 +451,72 @@ class LangGraphRunner(BaseRunner):
301
451
  )
302
452
  return local_agent
303
453
 
454
+ def _resolve_tool_output_manager(
455
+ self,
456
+ agent: Agent,
457
+ merged_agent_config: dict[str, Any],
458
+ shared_tool_output_manager: Any | None,
459
+ ) -> Any | None:
460
+ """Resolve tool output manager for local agent execution."""
461
+ tool_output_sharing_enabled = merged_agent_config.get("tool_output_sharing", False)
462
+ if not tool_output_sharing_enabled:
463
+ return None
464
+ if shared_tool_output_manager is not None:
465
+ return shared_tool_output_manager
466
+ return build_tool_output_manager(agent.name, merged_agent_config)
467
+
468
+ def _inject_hitl_manager(
469
+ self,
470
+ local_agent: Any,
471
+ merged_agent_config: dict[str, Any],
472
+ agent_name: str,
473
+ pause_resume_callback: Any | None,
474
+ ) -> None:
475
+ """Inject HITL manager when enabled, mirroring remote gating behavior."""
476
+ hitl_enabled = merged_agent_config.get("hitl_enabled", False)
477
+ if hitl_enabled:
478
+ try:
479
+ from aip_agents.agent.hitl.manager import ( # noqa: PLC0415
480
+ ApprovalManager,
481
+ )
482
+
483
+ from glaip_sdk.hitl import LocalPromptHandler # noqa: PLC0415
484
+
485
+ local_agent.hitl_manager = ApprovalManager(
486
+ prompt_handler=LocalPromptHandler(pause_resume_callback=pause_resume_callback)
487
+ )
488
+ # Store callback reference for setting renderer later
489
+ if pause_resume_callback:
490
+ local_agent._pause_resume_callback = pause_resume_callback
491
+ logger.debug(
492
+ "HITL manager injected for agent '%s' (hitl_enabled=True)",
493
+ agent_name,
494
+ )
495
+ except ImportError as e:
496
+ # Missing dependencies - fail fast
497
+ raise ImportError("Local HITL requires aip_agents. Install with: pip install 'glaip-sdk[local]'") from e
498
+ except Exception as e:
499
+ # Other errors during HITL setup - fail fast
500
+ raise RuntimeError(f"Failed to initialize HITL manager for agent '{agent_name}'") from e
501
+ else:
502
+ logger.debug(
503
+ "HITL manager not injected for agent '%s' (hitl_enabled=False)",
504
+ agent_name,
505
+ )
506
+
304
507
  def _build_sub_agents(
305
508
  self,
306
509
  sub_agents: list[Any] | None,
307
510
  runtime_config: dict[str, Any] | None,
511
+ shared_tool_output_manager: Any | None = None,
308
512
  ) -> list[Any]:
309
513
  """Build sub-agent instances recursively.
310
514
 
311
515
  Args:
312
516
  sub_agents: List of sub-agent definitions.
313
517
  runtime_config: Runtime config to pass to sub-agents.
518
+ shared_tool_output_manager: Optional ToolOutputManager to reuse across
519
+ agents with tool_output_sharing enabled.
314
520
 
315
521
  Returns:
316
522
  List of built sub-agent instances.
@@ -324,7 +530,13 @@ class LangGraphRunner(BaseRunner):
324
530
  sub_agent_instances = []
325
531
  for sub_agent in sub_agents:
326
532
  self._validate_sub_agent_for_local_mode(sub_agent)
327
- sub_agent_instances.append(self.build_langgraph_agent(sub_agent, runtime_config))
533
+ sub_agent_instances.append(
534
+ self.build_langgraph_agent(
535
+ sub_agent,
536
+ runtime_config,
537
+ shared_tool_output_manager=shared_tool_output_manager,
538
+ )
539
+ )
328
540
  return sub_agent_instances
329
541
 
330
542
  def _add_mcp_servers(
@@ -385,6 +597,14 @@ class LangGraphRunner(BaseRunner):
385
597
  if not runtime_config:
386
598
  return {}
387
599
 
600
+ # Check for unsupported runtime_config.ptc (v1 constraint)
601
+ if "ptc" in runtime_config:
602
+ validate_ptc_for_local_run(
603
+ agent_ptc=None,
604
+ agent_config_ptc=None,
605
+ runtime_config_ptc=runtime_config["ptc"],
606
+ )
607
+
388
608
  # 1. Extract global configs and normalize keys
389
609
  global_tool_configs = normalize_local_config_keys(runtime_config.get("tool_configs", {}))
390
610
  global_mcp_configs = normalize_local_config_keys(runtime_config.get("mcp_configs", {}))
@@ -544,6 +764,14 @@ class LangGraphRunner(BaseRunner):
544
764
  # Get runtime agent_config
545
765
  runtime_agent_config = normalized_config.get("agent_config", {})
546
766
 
767
+ # Check for unsupported agent_config.ptc (local runs constraint)
768
+ if "ptc" in agent_agent_config or "ptc" in runtime_agent_config:
769
+ validate_ptc_for_local_run(
770
+ agent_ptc=None,
771
+ agent_config_ptc=agent_agent_config.get("ptc") or runtime_agent_config.get("ptc"),
772
+ runtime_config_ptc=None,
773
+ )
774
+
547
775
  # Merge: agent definition < runtime config
548
776
  return merge_configs(agent_agent_config, runtime_agent_config)
549
777
 
@@ -566,12 +794,20 @@ class LangGraphRunner(BaseRunner):
566
794
  """
567
795
  direct_params = {}
568
796
  kwargs_params = {}
797
+ config_dict = {}
569
798
 
570
799
  # Direct constructor parameters
571
800
  if "planning" in agent_config:
572
801
  direct_params["planning"] = agent_config["planning"]
573
802
 
803
+ if "enable_a2a_token_streaming" in agent_config:
804
+ direct_params["enable_a2a_token_streaming"] = agent_config["enable_a2a_token_streaming"]
805
+
574
806
  # Kwargs parameters (passed through **kwargs to BaseAgent)
807
+ if "enable_pii" in agent_config:
808
+ kwargs_params["enable_pii"] = agent_config["enable_pii"]
809
+ config_dict["enable_pii"] = agent_config["enable_pii"]
810
+
575
811
  if "memory" in agent_config:
576
812
  # Map "memory" to "memory_backend" for aip-agents compatibility
577
813
  kwargs_params["memory_backend"] = agent_config["memory"]
@@ -582,8 +818,73 @@ class LangGraphRunner(BaseRunner):
582
818
  if key in agent_config:
583
819
  kwargs_params[key] = agent_config[key]
584
820
 
821
+ # Ensure we pass a config dictionary to BaseAgent, which uses it for
822
+ # LM configuration (api keys, etc.). Memory settings are passed only
823
+ # via kwargs to avoid leaking into LM invoker config.
824
+ if config_dict:
825
+ kwargs_params["config"] = config_dict
826
+
585
827
  return direct_params, kwargs_params
586
828
 
829
+ def _convert_model_for_local(self, model: Any) -> tuple[str, dict[str, Any]]:
830
+ """Convert model to aip_agents format for local execution.
831
+
832
+ Args:
833
+ model: Model object or string identifier.
834
+
835
+ Returns:
836
+ Tuple of (model_string, config_dict).
837
+ """
838
+ from glaip_sdk.models._validation import ( # noqa: PLC0415
839
+ convert_model_for_local_execution,
840
+ )
841
+
842
+ return convert_model_for_local_execution(model)
843
+
844
+ def _resolve_local_model(self, agent: Agent, agent_config_kwargs: dict[str, Any]) -> str:
845
+ """Resolve model string and merge its configuration into agent kwargs.
846
+
847
+ This method extracts model-specific credentials and hyperparameters from a Model
848
+ object and merges them into the 'config' dictionary within agent_config_kwargs.
849
+ This is required because BaseAgent expects LM settings (api keys, etc.) to be
850
+ inside the 'config' parameter, not top-level kwargs.
851
+
852
+ Example:
853
+ If agent has:
854
+ - model = Model(id="deepinfra/model", credentials="key-123")
855
+ - agent_config_kwargs = {"enable_pii": True, "config": {"enable_pii": True}}
856
+
857
+ _resolve_local_model will:
858
+ 1. Resolve model_string to "openai-compatible/model"
859
+ 2. Extract model_config as {"lm_api_key": "key-123"}
860
+ 3. Update agent_config_kwargs["config"] to:
861
+ {"enable_pii": True, "lm_api_key": "key-123"}
862
+
863
+ Args:
864
+ agent: The glaip_sdk Agent.
865
+ agent_config_kwargs: Agent config kwargs to update (modified in-place).
866
+
867
+ Returns:
868
+ The model identifier string for local execution.
869
+ """
870
+ model_to_use = agent.model or self.default_model
871
+ model_string, model_config = self._convert_model_for_local(model_to_use)
872
+
873
+ if model_config:
874
+ # Normalize config to a dict early to simplify merging
875
+ config_val = agent_config_kwargs.get("config", {})
876
+ if hasattr(config_val, "model_dump"):
877
+ config_val = config_val.model_dump()
878
+
879
+ if not isinstance(config_val, dict):
880
+ config_val = {}
881
+
882
+ # Use a single merge path for model configuration
883
+ config_val.update(model_config)
884
+ agent_config_kwargs["config"] = config_val
885
+
886
+ return model_string
887
+
587
888
  def _apply_runtime_mcp_configs(
588
889
  self,
589
890
  base_configs: dict[str, Any],
@@ -612,39 +913,126 @@ class LangGraphRunner(BaseRunner):
612
913
  base_config: dict[str, Any],
613
914
  override: dict[str, Any] | None,
614
915
  ) -> dict[str, Any]:
615
- """Merge a single MCP config with runtime override.
916
+ """Merge a single MCP config with a runtime override, handling normalization and parity fixes.
917
+
918
+ This method orchestrates the merging of base MCP settings (from the object definition)
919
+ with runtime overrides. It enforces Platform parity by prioritizing the nested 'config'
920
+ block while maintaining robustness for local development by auto-fixing flat transport keys.
921
+
922
+ The merge follows these priority rules (highest to lowest):
923
+ 1. Misplaced flat keys in the override (e.g., 'url' at top level) - Auto-fixed with warning.
924
+ 2. Nested 'config' block in the override (Matches Platform/Constructor schema).
925
+ 3. Authentication objects in the override (Converted to HTTP headers).
926
+ 4. Structural settings in the override (e.g., 'allowed_tools').
927
+ 5. Base configuration from the MCP object definition.
928
+
929
+ Examples:
930
+ >>> # 1. Strict Nested Style (Recommended)
931
+ >>> override = {"config": {"url": "https://new.api"}, "allowed_tools": ["t1"]}
932
+ >>> self._merge_single_mcp_config("mcp", base, override)
933
+ >>> # Result: {"url": "https://new.api", "allowed_tools": ["t1"], ...}
934
+
935
+ >>> # 2. Flat Legacy Style (Auto-fixed with warning)
936
+ >>> override = {"url": "https://new.api"}
937
+ >>> self._merge_single_mcp_config("mcp", base, override)
938
+ >>> # Result: {"url": "https://new.api", ...}
939
+
940
+ >>> # 3. Header Merging (Preserves Auth)
941
+ >>> base = {"headers": {"Authorization": "Bearer token"}}
942
+ >>> override = {"headers": {"X-Custom": "val"}}
943
+ >>> self._merge_single_mcp_config("mcp", base, override)
944
+ >>> # Result: {"headers": {"Authorization": "Bearer token", "X-Custom": "val"}, ...}
616
945
 
617
946
  Args:
618
- server_name: Name of the MCP server.
619
- base_config: Base config from adapter.
620
- override: Optional runtime override config.
947
+ server_name: Name of the MCP server being configured.
948
+ base_config: Base configuration dictionary derived from the MCP object.
949
+ override: Optional dictionary of runtime overrides.
621
950
 
622
951
  Returns:
623
- Merged config dict.
952
+ A fully merged and normalized configuration dictionary ready for the local runner.
624
953
  """
625
954
  merged = base_config.copy()
626
955
 
627
956
  if not override:
628
957
  return merged
629
958
 
630
- from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
631
- MCPConfigBuilder,
632
- )
959
+ # 1. Check for misplaced keys and warn (DX/Parity guidance)
960
+ self._warn_if_mcp_override_misplaced(server_name, override)
633
961
 
634
- # Handle authentication override
635
- if "authentication" in override:
636
- headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
637
- if headers:
638
- merged["headers"] = headers
639
- logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
962
+ # 2. Apply Authentication (Converted to headers)
963
+ self._apply_mcp_auth_override(server_name, merged, override)
640
964
 
641
- # Merge other config keys (excluding authentication since we converted it)
965
+ # 3. Apply Transport Settings (Nested 'config')
966
+ if "config" in override and isinstance(override["config"], dict):
967
+ merged.update(override["config"])
968
+
969
+ # 4. Apply Structural Settings (e.g., allowed_tools)
970
+ if "allowed_tools" in override:
971
+ merged["allowed_tools"] = override["allowed_tools"]
972
+
973
+ # 5. Preserve unknown top-level keys (backward compatibility)
974
+ known_keys = _MCP_TRANSPORT_KEYS | {"config", "authentication", "allowed_tools"}
642
975
  for key, value in override.items():
643
- if key != "authentication":
976
+ if key not in known_keys:
644
977
  merged[key] = value
645
978
 
979
+ # 6. Apply Auto-fix for misplaced keys (Local Success)
980
+ for key in [k for k in override if k in _MCP_TRANSPORT_KEYS]:
981
+ val = override[key]
982
+ # Special case: Merge headers instead of overwriting to preserve auth
983
+ if key == "headers" and isinstance(val, dict) and isinstance(merged.get("headers"), dict):
984
+ merged["headers"].update(val)
985
+ else:
986
+ merged[key] = val
987
+
646
988
  return merged
647
989
 
990
+ def _warn_if_mcp_override_misplaced(self, server_name: str, override: dict[str, Any]) -> None:
991
+ """Log a warning if transport keys are found at the top level of an override.
992
+
993
+ Args:
994
+ server_name: Name of the MCP server.
995
+ override: The raw override dictionary.
996
+ """
997
+ misplaced = [k for k in override if k in _MCP_TRANSPORT_KEYS]
998
+ if misplaced:
999
+ logger.warning(
1000
+ "MCP '%s' override contains transport keys at the top level: %s. "
1001
+ "This structure is inconsistent with the Platform and MCP constructor. "
1002
+ "Transport settings should be nested within a 'config' dictionary. "
1003
+ "Example: mcp_configs={'%s': {'config': {'%s': '...'}}}. "
1004
+ "Automatically merging top-level keys for local execution parity.",
1005
+ server_name,
1006
+ misplaced,
1007
+ server_name,
1008
+ misplaced[0],
1009
+ )
1010
+
1011
+ def _apply_mcp_auth_override(
1012
+ self,
1013
+ server_name: str,
1014
+ merged_config: dict[str, Any],
1015
+ override: dict[str, Any],
1016
+ ) -> None:
1017
+ """Convert authentication override to headers and apply to config.
1018
+
1019
+ Args:
1020
+ server_name: Name of the MCP server.
1021
+ merged_config: The configuration being built (mutated in place).
1022
+ override: The raw override dictionary.
1023
+ """
1024
+ if "authentication" not in override:
1025
+ return
1026
+
1027
+ from glaip_sdk.runner.mcp_adapter.mcp_config_builder import ( # noqa: PLC0415
1028
+ MCPConfigBuilder,
1029
+ )
1030
+
1031
+ headers = MCPConfigBuilder.build_headers_from_auth(override["authentication"])
1032
+ if headers:
1033
+ merged_config["headers"] = headers
1034
+ logger.debug("Applied runtime authentication headers for MCP '%s'", server_name)
1035
+
648
1036
  def _validate_sub_agent_for_local_mode(self, sub_agent: Any) -> None:
649
1037
  """Validate that a sub-agent reference is supported for local execution.
650
1038